Commit 309c4d1b authored by Lukas Eipert's avatar Lukas Eipert Committed by Filipa Lacerda

Add Manage licenses to MR widget and pipelines

parent 9afa4a5f
...@@ -139,7 +139,7 @@ export default { ...@@ -139,7 +139,7 @@ export default {
<section class="media-section"> <section class="media-section">
<div class="media"> <div class="media">
<status-icon :status="statusIconName" /> <status-icon :status="statusIconName" />
<div class="media-body space-children d-flex flex-align-self-center"> <div class="media-body d-flex flex-align-self-center">
<span class="js-code-text code-text"> <span class="js-code-text code-text">
{{ headerText }} {{ headerText }}
......
...@@ -10,7 +10,12 @@ export default () => { ...@@ -10,7 +10,12 @@ export default () => {
const licensesTab = document.getElementById('js-licenses-app'); const licensesTab = document.getElementById('js-licenses-app');
if (licensesTab) { if (licensesTab) {
const { licenseHeadPath, canManageLicenses, apiUrl } = licensesTab.dataset; const {
licenseHeadPath,
canManageLicenses,
apiUrl,
licenseManagementSettingsPath,
} = licensesTab.dataset;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
...@@ -22,13 +27,14 @@ export default () => { ...@@ -22,13 +27,14 @@ export default () => {
return createElement('license-report-app', { return createElement('license-report-app', {
props: { props: {
apiUrl, apiUrl,
licenseManagementSettingsPath,
headPath: licenseHeadPath, headPath: licenseHeadPath,
canManageLicenses: convertPermissionToBoolean(canManageLicenses), canManageLicenses: convertPermissionToBoolean(canManageLicenses),
alwaysOpen: true, alwaysOpen: true,
reportSectionClass: 'split-report-section', reportSectionClass: 'split-report-section',
}, },
on: { on: {
updateBadgeCount: (count) => { updateBadgeCount: count => {
updateBadgeCount('.js-licenses-counter', count); updateBadgeCount('.js-licenses-counter', count);
}, },
}, },
......
...@@ -261,6 +261,8 @@ export default { ...@@ -261,6 +261,8 @@ export default {
:api-url="mr.licenseManagement.managed_licenses_path" :api-url="mr.licenseManagement.managed_licenses_path"
:pipeline-path="mr.pipeline.path" :pipeline-path="mr.pipeline.path"
:can-manage-licenses="mr.licenseManagement.can_manage_licenses" :can-manage-licenses="mr.licenseManagement.can_manage_licenses"
:full-report-path="mr.licenseManagement.license_management_full_report_path"
:license-management-settings-path="mr.licenseManagement.license_management_settings_path"
:base-path="mr.licenseManagement.base_path" :base-path="mr.licenseManagement.base_path"
:head-path="mr.licenseManagement.head_path" :head-path="mr.licenseManagement.head_path"
report-section-class="mr-widget-border-top" report-section-class="mr-widget-border-top"
......
...@@ -30,7 +30,12 @@ export default { ...@@ -30,7 +30,12 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
pipelinePath: { fullReportPath: {
type: String,
required: false,
default: null,
},
licenseManagementSettingsPath: {
type: String, type: String,
required: false, required: false,
default: null, default: null,
...@@ -64,8 +69,8 @@ export default { ...@@ -64,8 +69,8 @@ export default {
licenseReportStatus() { licenseReportStatus() {
return this.checkReportStatus(this.isLoading, this.loadLicenseReportError); return this.checkReportStatus(this.isLoading, this.loadLicenseReportError);
}, },
licensesTab() { showActionButtons() {
return this.pipelinePath ? `${this.pipelinePath}/licenses` : null; return this.licenseManagementSettingsPath !== null || this.fullReportPath !== null;
}, },
}, },
watch: { watch: {
...@@ -107,15 +112,25 @@ export default { ...@@ -107,15 +112,25 @@ export default {
class="license-report-widget mr-report" class="license-report-widget mr-report"
> >
<div <div
v-if="licensesTab" v-if="showActionButtons"
slot="actionButtons" slot="actionButtons"
class="append-right-default"
> >
<a <a
:href="licensesTab" v-if="licenseManagementSettingsPath"
:class="{'append-right-8': fullReportPath}"
:href="licenseManagementSettingsPath"
class="btn btn-default btn-sm js-manage-licenses"
>
{{ s__("ciReport|Manage licenses") }}
</a>
<a
v-if="fullReportPath"
:href="fullReportPath"
target="_blank" target="_blank"
class="btn btn-default btn-sm float-right" class="btn btn-default btn-sm js-full-report"
> >
<span>{{ s__("ciReport|View full report") }}</span> {{ s__("ciReport|View full report") }}
<icon <icon
:size="16" :size="16"
name="external-link" name="external-link"
......
...@@ -227,7 +227,7 @@ export default { ...@@ -227,7 +227,7 @@ export default {
<a <a
:href="securityTab" :href="securityTab"
target="_blank" target="_blank"
class="btn btn-default btn-sm float-right" class="btn btn-default btn-sm float-right append-right-default"
> >
<span>{{ s__("ciReport|View full report") }}</span> <span>{{ s__("ciReport|View full report") }}</span>
<icon <icon
......
...@@ -69,5 +69,9 @@ module EE ...@@ -69,5 +69,9 @@ module EE
def license_management_api_url(project) def license_management_api_url(project)
api_v4_projects_managed_licenses_path(id: project.id) api_v4_projects_managed_licenses_path(id: project.id)
end end
def license_management_settings_path(project)
project_settings_ci_cd_path(project, anchor: 'js-license-management')
end
end end
end end
...@@ -108,6 +108,14 @@ module EE ...@@ -108,6 +108,14 @@ module EE
expose :can_manage_licenses do |merge_request| expose :can_manage_licenses do |merge_request|
can?(current_user, :admin_software_license_policy, merge_request) can?(current_user, :admin_software_license_policy, merge_request)
end end
expose :license_management_settings_path, if: -> (mr, _) {can?(current_user, :admin_software_license_policy, mr.target_project)} do |merge_request|
license_management_settings_path(merge_request.target_project)
end
expose :license_management_full_report_path, if: -> (mr, _) { mr.head_pipeline } do |merge_request|
licenses_project_pipeline_path(merge_request.target_project, merge_request.head_pipeline)
end
end end
# expose_sast_container_data? is deprecated and replaced with expose_container_scanning_data? (#5778) # expose_sast_container_data? is deprecated and replaced with expose_container_scanning_data? (#5778)
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
- dast_endpoint = pipeline.expose_dast_data? ? dast_artifact_url(pipeline) : nil - dast_endpoint = pipeline.expose_dast_data? ? dast_artifact_url(pipeline) : nil
- sast_container_endpoint = pipeline.expose_sast_container_data? ? sast_container_artifact_url(pipeline) : pipeline.expose_container_scanning_data? ? container_scanning_artifact_url(pipeline) : nil - sast_container_endpoint = pipeline.expose_sast_container_data? ? sast_container_artifact_url(pipeline) : pipeline.expose_container_scanning_data? ? container_scanning_artifact_url(pipeline) : nil
- blob_path = project_blob_path(project, pipeline.sha) - blob_path = project_blob_path(project, pipeline.sha)
- license_management_settings_path = can?(current_user, :admin_software_license_policy, project) ? license_management_settings_path(project) : nil
- if pipeline.expose_security_dashboard? - if pipeline.expose_security_dashboard?
#js-tab-security.build-security.tab-pane #js-tab-security.build-security.tab-pane
...@@ -28,4 +29,5 @@ ...@@ -28,4 +29,5 @@
#js-tab-licenses.tab-pane #js-tab-licenses.tab-pane
#js-licenses-app{ data: { license_head_path: pipeline.expose_license_management_data? ? license_management_artifact_url(pipeline) : nil, #js-licenses-app{ data: { license_head_path: pipeline.expose_license_management_data? ? license_management_artifact_url(pipeline) : nil,
api_url: license_management_api_url(project), api_url: license_management_api_url(project),
license_management_settings_path: license_management_settings_path,
can_manage_licenses: can?(current_user, :admin_software_license_policy, project).to_s } } can_manage_licenses: can?(current_user, :admin_software_license_policy, project).to_s } }
- return unless @project.feature_available?(:license_management) - return unless @project.feature_available?(:license_management)
- expanded = Rails.env.test? - expanded = Rails.env.test?
%section.settings.no-animate{ class: ('expanded' if expanded) } %section.settings.no-animate#js-license-management{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
= s_('LicenseManagement|License Management') = s_('LicenseManagement|License Management')
......
---
title: Add `Manage licenses` button to MR widget and pipelines view
merge_request: 7411
author:
type: added
...@@ -15,25 +15,14 @@ describe('License Report MR Widget', () => { ...@@ -15,25 +15,14 @@ describe('License Report MR Widget', () => {
const Component = Vue.extend(LicenseManagement); const Component = Vue.extend(LicenseManagement);
const apiUrl = `${TEST_HOST}/license_management`; const apiUrl = `${TEST_HOST}/license_management`;
let vm; let vm;
let store;
let actions;
let getters;
const props = {
loadingText: 'LOADING',
errorText: 'ERROR',
headPath: `${TEST_HOST}/head.json`,
basePath: `${TEST_HOST}/head.json`,
canManageLicenses: true,
apiUrl,
};
beforeEach(() => { const defaultState = {
actions = { managedLicenses: [approvedLicense, blacklistedLicense],
setAPISettings: jasmine.createSpy('setAPISettings').and.callFake(() => {}), currentLicenseInModal: licenseReportMock[0],
loadManagedLicenses: jasmine.createSpy('loadManagedLicenses').and.callFake(() => {}), isLoadingManagedLicenses: true,
loadLicenseReport: jasmine.createSpy('loadLicenseReport').and.callFake(() => {}),
}; };
getters = {
const defaultGetters = {
isLoading() { isLoading() {
return false; return false;
}, },
...@@ -44,16 +33,40 @@ describe('License Report MR Widget', () => { ...@@ -44,16 +33,40 @@ describe('License Report MR Widget', () => {
return 'FOO'; return 'FOO';
}, },
}; };
store = new Vuex.Store({
state: { const defaultProps = {
managedLicenses: [approvedLicense, blacklistedLicense], loadingText: 'LOADING',
currentLicenseInModal: licenseReportMock[0], errorText: 'ERROR',
isLoadingManagedLicenses: true, headPath: `${TEST_HOST}/head.json`,
}, basePath: `${TEST_HOST}/head.json`,
canManageLicenses: true,
licenseManagementSettingsPath: `${TEST_HOST}/lm_settings`,
fullReportPath: `${TEST_HOST}/path/to/the/full/report`,
apiUrl,
};
const defaultActions = {
setAPISettings: () => {},
loadManagedLicenses: () => {},
loadLicenseReport: () => {},
};
const mountComponent = ({
props = defaultProps,
getters = defaultGetters,
state = defaultState,
actions = defaultActions,
} = {}) => {
const store = new Vuex.Store({
state,
getters, getters,
actions, actions,
}); });
vm = mountComponentWithStore(Component, { props, store }); return mountComponentWithStore(Component, { props, store });
};
beforeEach(() => {
vm = mountComponent();
}); });
afterEach(() => { afterEach(() => {
...@@ -62,106 +75,153 @@ describe('License Report MR Widget', () => { ...@@ -62,106 +75,153 @@ describe('License Report MR Widget', () => {
describe('computed', () => { describe('computed', () => {
describe('hasLicenseReportIssues', () => { describe('hasLicenseReportIssues', () => {
it('should be false, if the report is empty', done => { it('should be false, if the report is empty', () => {
store.hotUpdate({ const getters = {
getters: { ...defaultGetters,
...getters,
licenseReport() { licenseReport() {
return []; return [];
}, },
}, };
}); vm = mountComponent({ getters });
return Vue.nextTick().then(() => {
expect(vm.hasLicenseReportIssues).toBe(false); expect(vm.hasLicenseReportIssues).toBe(false);
done();
});
}); });
it('should be true, if the report is not empty', done => it('should be true, if the report is not empty', () => {
Vue.nextTick().then(() => {
expect(vm.hasLicenseReportIssues).toBe(true); expect(vm.hasLicenseReportIssues).toBe(true);
done(); });
})); });
describe('licenseReportStatus', () => {
it('should be `LOADING`, if the report is loading', () => {
const getters = {
...defaultGetters,
isLoading() {
return true;
},
};
vm = mountComponent({ getters });
expect(vm.licenseReportStatus).toBe(LOADING);
}); });
describe('licensesTab', () => { it('should be `ERROR`, if the report is has an error', () => {
it('with the pipelinePath prop', done => { const state = { ...defaultState, loadLicenseReportError: new Error('test') };
const pipelinePath = `${TEST_HOST}/path/to/the/pipeline`; vm = mountComponent({ state });
vm.pipelinePath = pipelinePath; expect(vm.licenseReportStatus).toBe(ERROR);
});
return Vue.nextTick().then(() => { it('should be `SUCCESS`, if the report is successful', () => {
expect(vm.licensesTab).toEqual(`${pipelinePath}/licenses`); expect(vm.licenseReportStatus).toBe(SUCCESS);
done();
}); });
}); });
it('without the pipelinePath prop', () => { describe('showActionButtons', () => {
expect(vm.licensesTab).toEqual(null); const { fullReportPath, licenseManagementSettingsPath, ...otherProps } = defaultProps;
it('should be true if fullReportPath AND licenseManagementSettingsPath prop are provided', () => {
const props = { ...otherProps, fullReportPath, licenseManagementSettingsPath };
vm = mountComponent({ props });
expect(vm.showActionButtons).toBe(true);
}); });
it('should be true if only licenseManagementSettingsPath is provided', () => {
const props = { ...otherProps, fullReportPath: null, licenseManagementSettingsPath };
vm = mountComponent({ props });
expect(vm.showActionButtons).toBe(true);
}); });
describe('licenseReportStatus', () => { it('should be true if only fullReportPath is provided', () => {
it('should be `LOADING`, if the report is loading', done => { const props = {
store.hotUpdate({ ...otherProps,
getters: { fullReportPath,
...getters, licenseManagementSettingsPath: null,
isLoading() { };
return true; vm = mountComponent({ props });
},
}, expect(vm.showActionButtons).toBe(true);
});
it('should be false if fullReportPath and licenseManagementSettingsPath prop are not provided', () => {
const props = {
...otherProps,
fullReportPath: null,
licenseManagementSettingsPath: null,
};
vm = mountComponent({ props });
expect(vm.showActionButtons).toBe(false);
}); });
return Vue.nextTick().then(() => {
expect(vm.licenseReportStatus).toBe(LOADING);
done();
}); });
}); });
it('should be `ERROR`, if the report is has an error', done => { it('should render report section wrapper', () => {
store.replaceState({ ...store.state, loadLicenseReportError: new Error('test') }); expect(vm.$el.querySelector('.license-report-widget')).not.toBeNull();
return Vue.nextTick().then(() => {
expect(vm.licenseReportStatus).toBe(ERROR);
done();
}); });
it('should render report widget section', () => {
expect(vm.$el.querySelector('.report-block-container')).not.toBeNull();
}); });
it('should be `SUCCESS`, if the report is successful', done => describe('`View full report` button', () => {
Vue.nextTick().then(() => { const selector = '.js-full-report';
expect(vm.licenseReportStatus).toBe(SUCCESS);
done(); it('should be rendered when fullReportPath prop is provided', () => {
})); const linkEl = vm.$el.querySelector(selector);
expect(linkEl).not.toBeNull();
expect(linkEl.getAttribute('href')).toEqual(defaultProps.fullReportPath);
expect(linkEl.textContent.trim()).toEqual('View full report');
});
it('should not be rendered when fullReportPath prop is not provided', () => {
const props = { ...defaultProps, fullReportPath: null };
vm = mountComponent({ props });
const linkEl = vm.$el.querySelector(selector);
expect(linkEl).toBeNull();
}); });
}); });
it('should render report section wrapper', done => describe('`Manage licenses` button', () => {
Vue.nextTick().then(() => { const selector = '.js-manage-licenses';
expect(vm.$el.querySelector('.license-report-widget')).not.toBeNull();
done();
}));
it('should render report widget section', done => it('should be rendered when licenseManagementSettingsPath prop is provided', () => {
Vue.nextTick().then(() => { const linkEl = vm.$el.querySelector(selector);
expect(vm.$el.querySelector('.report-block-container')).not.toBeNull(); expect(linkEl).not.toBeNull();
done(); expect(linkEl.getAttribute('href')).toEqual(defaultProps.licenseManagementSettingsPath);
})); expect(linkEl.textContent.trim()).toEqual('Manage licenses');
});
it('should render set approval modal', done => { it('should not be rendered when licenseManagementSettingsPath prop is not provided', () => {
store.replaceState({ ...store.state }); const props = { ...defaultProps, licenseManagementSettingsPath: null };
vm = mountComponent({ props });
return Vue.nextTick().then(() => { const linkEl = vm.$el.querySelector(selector);
expect(vm.$el.querySelector('#modal-set-license-approval')).not.toBeNull(); expect(linkEl).toBeNull();
done(); });
}); });
it('should render set approval modal', () => {
expect(vm.$el.querySelector('#modal-set-license-approval')).not.toBeNull();
}); });
it('should init store after mount', () => it('should init store after mount', () => {
Vue.nextTick().then(() => { const actions = {
setAPISettings: jasmine.createSpy('setAPISettings').and.callFake(() => {}),
loadManagedLicenses: jasmine.createSpy('loadManagedLicenses').and.callFake(() => {}),
loadLicenseReport: jasmine.createSpy('loadLicenseReport').and.callFake(() => {}),
};
vm = mountComponent({ actions });
expect(actions.setAPISettings).toHaveBeenCalledWith( expect(actions.setAPISettings).toHaveBeenCalledWith(
jasmine.any(Object), jasmine.any(Object),
{ {
apiUrlManageLicenses: apiUrl, apiUrlManageLicenses: apiUrl,
headPath: props.headPath, headPath: defaultProps.headPath,
basePath: props.basePath, basePath: defaultProps.basePath,
canManageLicenses: true, canManageLicenses: true,
}, },
undefined, undefined,
...@@ -176,5 +236,5 @@ describe('License Report MR Widget', () => { ...@@ -176,5 +236,5 @@ describe('License Report MR Widget', () => {
undefined, undefined,
undefined, undefined,
); );
})); });
}); });
...@@ -102,7 +102,8 @@ describe MergeRequestWidgetEntity do ...@@ -102,7 +102,8 @@ describe MergeRequestWidgetEntity do
expect(subject.as_json[:dependency_scanning]).to include(:base_path) expect(subject.as_json[:dependency_scanning]).to include(:base_path)
end end
it 'has license_management data' do describe '#license_management' do
before do
build = create(:ci_build, name: 'license_management', pipeline: pipeline) build = create(:ci_build, name: 'license_management', pipeline: pipeline)
allow(merge_request).to receive_messages( allow(merge_request).to receive_messages(
...@@ -110,14 +111,37 @@ describe MergeRequestWidgetEntity do ...@@ -110,14 +111,37 @@ describe MergeRequestWidgetEntity do
expose_security_dashboard?: false, expose_security_dashboard?: false,
base_has_license_management_data?: true, base_has_license_management_data?: true,
base_license_management_artifact: build, base_license_management_artifact: build,
head_license_management_artifact: build head_license_management_artifact: build,
head_pipeline: pipeline,
target_project: project
) )
end
it 'should not be included, if license management features are off' do
allow(merge_request).to receive_messages(expose_license_management_data?: false)
expect(subject.as_json).not_to include(:license_management)
end
it 'should be included, if license manage management features are on' do
expect(subject.as_json).to include(:license_management) expect(subject.as_json).to include(:license_management)
expect(subject.as_json[:license_management]).to include(:head_path) expect(subject.as_json[:license_management]).to include(:head_path)
expect(subject.as_json[:license_management]).to include(:base_path) expect(subject.as_json[:license_management]).to include(:base_path)
expect(subject.as_json[:license_management]).to include(:managed_licenses_path) expect(subject.as_json[:license_management]).to include(:managed_licenses_path)
expect(subject.as_json[:license_management]).to include(:can_manage_licenses) expect(subject.as_json[:license_management]).to include(:can_manage_licenses)
expect(subject.as_json[:license_management]).to include(:license_management_full_report_path)
end
it '#license_management_settings_path should not be included for developers' do
expect(subject.as_json[:license_management]).not_to include(:license_management_settings_path)
end
it '#license_management_settings_path should be included for maintainers' do
stub_licensed_features(license_management: true)
project.add_maintainer(user)
expect(subject.as_json[:license_management]).to include(:license_management_settings_path)
end
end end
# methods for old artifact are deprecated and replaced with ones for the new name (#5779) # methods for old artifact are deprecated and replaced with ones for the new name (#5779)
......
...@@ -8745,6 +8745,9 @@ msgstr "" ...@@ -8745,6 +8745,9 @@ msgstr ""
msgid "ciReport|Loading %{reportName} report" msgid "ciReport|Loading %{reportName} report"
msgstr "" msgstr ""
msgid "ciReport|Manage licenses"
msgstr ""
msgid "ciReport|Method" msgid "ciReport|Method"
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment