Commit 2de53683 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '7120-add-a-manage-licenses-button-in-the-report' into 'master'

Add Manage licenses to MR widget and pipelines

Closes #7120

See merge request gitlab-org/gitlab-ee!7411
parents 9afa4a5f 309c4d1b
...@@ -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,45 +15,58 @@ describe('License Report MR Widget', () => { ...@@ -15,45 +15,58 @@ 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; const defaultState = {
let getters; managedLicenses: [approvedLicense, blacklistedLicense],
const props = { currentLicenseInModal: licenseReportMock[0],
isLoadingManagedLicenses: true,
};
const defaultGetters = {
isLoading() {
return false;
},
licenseReport() {
return licenseReportMock;
},
licenseSummaryText() {
return 'FOO';
},
};
const defaultProps = {
loadingText: 'LOADING', loadingText: 'LOADING',
errorText: 'ERROR', errorText: 'ERROR',
headPath: `${TEST_HOST}/head.json`, headPath: `${TEST_HOST}/head.json`,
basePath: `${TEST_HOST}/head.json`, basePath: `${TEST_HOST}/head.json`,
canManageLicenses: true, canManageLicenses: true,
licenseManagementSettingsPath: `${TEST_HOST}/lm_settings`,
fullReportPath: `${TEST_HOST}/path/to/the/full/report`,
apiUrl, apiUrl,
}; };
beforeEach(() => { const defaultActions = {
actions = { setAPISettings: () => {},
setAPISettings: jasmine.createSpy('setAPISettings').and.callFake(() => {}), loadManagedLicenses: () => {},
loadManagedLicenses: jasmine.createSpy('loadManagedLicenses').and.callFake(() => {}), loadLicenseReport: () => {},
loadLicenseReport: jasmine.createSpy('loadLicenseReport').and.callFake(() => {}), };
};
getters = { const mountComponent = ({
isLoading() { props = defaultProps,
return false; getters = defaultGetters,
}, state = defaultState,
licenseReport() { actions = defaultActions,
return licenseReportMock; } = {}) => {
}, const store = new Vuex.Store({
licenseSummaryText() { state,
return 'FOO';
},
};
store = new Vuex.Store({
state: {
managedLicenses: [approvedLicense, blacklistedLicense],
currentLicenseInModal: licenseReportMock[0],
isLoadingManagedLicenses: true,
},
getters, getters,
actions, actions,
}); });
vm = mountComponentWithStore(Component, { props, store }); return mountComponentWithStore(Component, { props, store });
};
beforeEach(() => {
vm = mountComponent();
}); });
afterEach(() => { afterEach(() => {
...@@ -62,119 +75,166 @@ describe('License Report MR Widget', () => { ...@@ -62,119 +75,166 @@ 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 [];
},
}, },
}); };
return Vue.nextTick().then(() => { vm = mountComponent({ getters });
expect(vm.hasLicenseReportIssues).toBe(false);
done(); expect(vm.hasLicenseReportIssues).toBe(false);
});
}); });
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('licensesTab', () => { describe('licenseReportStatus', () => {
it('with the pipelinePath prop', done => { it('should be `LOADING`, if the report is loading', () => {
const pipelinePath = `${TEST_HOST}/path/to/the/pipeline`; const getters = {
...defaultGetters,
isLoading() {
return true;
},
};
vm = mountComponent({ getters });
vm.pipelinePath = pipelinePath; expect(vm.licenseReportStatus).toBe(LOADING);
});
return Vue.nextTick().then(() => { it('should be `ERROR`, if the report is has an error', () => {
expect(vm.licensesTab).toEqual(`${pipelinePath}/licenses`); const state = { ...defaultState, loadLicenseReportError: new Error('test') };
done(); vm = mountComponent({ state });
});
expect(vm.licenseReportStatus).toBe(ERROR);
}); });
it('without the pipelinePath prop', () => { it('should be `SUCCESS`, if the report is successful', () => {
expect(vm.licensesTab).toEqual(null); expect(vm.licenseReportStatus).toBe(SUCCESS);
}); });
}); });
describe('licenseReportStatus', () => { describe('showActionButtons', () => {
it('should be `LOADING`, if the report is loading', done => { const { fullReportPath, licenseManagementSettingsPath, ...otherProps } = defaultProps;
store.hotUpdate({
getters: { it('should be true if fullReportPath AND licenseManagementSettingsPath prop are provided', () => {
...getters, const props = { ...otherProps, fullReportPath, licenseManagementSettingsPath };
isLoading() { vm = mountComponent({ props });
return true;
}, expect(vm.showActionButtons).toBe(true);
}, });
});
return Vue.nextTick().then(() => { it('should be true if only licenseManagementSettingsPath is provided', () => {
expect(vm.licenseReportStatus).toBe(LOADING); const props = { ...otherProps, fullReportPath: null, licenseManagementSettingsPath };
done(); vm = mountComponent({ props });
});
expect(vm.showActionButtons).toBe(true);
});
it('should be true if only fullReportPath is provided', () => {
const props = {
...otherProps,
fullReportPath,
licenseManagementSettingsPath: null,
};
vm = mountComponent({ props });
expect(vm.showActionButtons).toBe(true);
}); });
it('should be `ERROR`, if the report is has an error', done => { it('should be false if fullReportPath and licenseManagementSettingsPath prop are not provided', () => {
store.replaceState({ ...store.state, loadLicenseReportError: new Error('test') }); const props = {
return Vue.nextTick().then(() => { ...otherProps,
expect(vm.licenseReportStatus).toBe(ERROR); fullReportPath: null,
done(); licenseManagementSettingsPath: null,
}); };
vm = mountComponent({ props });
expect(vm.showActionButtons).toBe(false);
}); });
});
});
it('should render report section wrapper', () => {
expect(vm.$el.querySelector('.license-report-widget')).not.toBeNull();
});
it('should render report widget section', () => {
expect(vm.$el.querySelector('.report-block-container')).not.toBeNull();
});
describe('`View full report` button', () => {
const selector = '.js-full-report';
it('should be `SUCCESS`, if the report is successful', done => it('should be rendered when fullReportPath prop is provided', () => {
Vue.nextTick().then(() => { const linkEl = vm.$el.querySelector(selector);
expect(vm.licenseReportStatus).toBe(SUCCESS); expect(linkEl).not.toBeNull();
done(); 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 init store after mount', () => it('should render set approval modal', () => {
Vue.nextTick().then(() => { expect(vm.$el.querySelector('#modal-set-license-approval')).not.toBeNull();
expect(actions.setAPISettings).toHaveBeenCalledWith( });
jasmine.any(Object),
{ it('should init store after mount', () => {
apiUrlManageLicenses: apiUrl, const actions = {
headPath: props.headPath, setAPISettings: jasmine.createSpy('setAPISettings').and.callFake(() => {}),
basePath: props.basePath, loadManagedLicenses: jasmine.createSpy('loadManagedLicenses').and.callFake(() => {}),
canManageLicenses: true, loadLicenseReport: jasmine.createSpy('loadLicenseReport').and.callFake(() => {}),
}, };
undefined, vm = mountComponent({ actions });
);
expect(actions.loadManagedLicenses).toHaveBeenCalledWith( expect(actions.setAPISettings).toHaveBeenCalledWith(
jasmine.any(Object), jasmine.any(Object),
undefined, {
undefined, apiUrlManageLicenses: apiUrl,
); headPath: defaultProps.headPath,
expect(actions.loadLicenseReport).toHaveBeenCalledWith( basePath: defaultProps.basePath,
jasmine.any(Object), canManageLicenses: true,
undefined, },
undefined, undefined,
); );
})); expect(actions.loadManagedLicenses).toHaveBeenCalledWith(
jasmine.any(Object),
undefined,
undefined,
);
expect(actions.loadLicenseReport).toHaveBeenCalledWith(
jasmine.any(Object),
undefined,
undefined,
);
});
}); });
...@@ -102,22 +102,46 @@ describe MergeRequestWidgetEntity do ...@@ -102,22 +102,46 @@ 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
build = create(:ci_build, name: 'license_management', pipeline: pipeline) before do
build = create(:ci_build, name: 'license_management', pipeline: pipeline)
allow(merge_request).to receive_messages(
expose_license_management_data?: true, allow(merge_request).to receive_messages(
expose_security_dashboard?: false, expose_license_management_data?: true,
base_has_license_management_data?: true, expose_security_dashboard?: false,
base_license_management_artifact: build, base_has_license_management_data?: true,
head_license_management_artifact: build base_license_management_artifact: build,
) head_license_management_artifact: build,
head_pipeline: pipeline,
expect(subject.as_json).to include(:license_management) target_project: project
expect(subject.as_json[:license_management]).to include(:head_path) )
expect(subject.as_json[:license_management]).to include(:base_path) end
expect(subject.as_json[:license_management]).to include(:managed_licenses_path)
expect(subject.as_json[:license_management]).to include(:can_manage_licenses) 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[:license_management]).to include(:head_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(: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