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 {
<section class="media-section">
<div class="media">
<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">
{{ headerText }}
......
......@@ -10,7 +10,12 @@ export default () => {
const licensesTab = document.getElementById('js-licenses-app');
if (licensesTab) {
const { licenseHeadPath, canManageLicenses, apiUrl } = licensesTab.dataset;
const {
licenseHeadPath,
canManageLicenses,
apiUrl,
licenseManagementSettingsPath,
} = licensesTab.dataset;
// eslint-disable-next-line no-new
new Vue({
......@@ -22,13 +27,14 @@ export default () => {
return createElement('license-report-app', {
props: {
apiUrl,
licenseManagementSettingsPath,
headPath: licenseHeadPath,
canManageLicenses: convertPermissionToBoolean(canManageLicenses),
alwaysOpen: true,
reportSectionClass: 'split-report-section',
},
on: {
updateBadgeCount: (count) => {
updateBadgeCount: count => {
updateBadgeCount('.js-licenses-counter', count);
},
},
......
......@@ -261,6 +261,8 @@ export default {
:api-url="mr.licenseManagement.managed_licenses_path"
:pipeline-path="mr.pipeline.path"
: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"
:head-path="mr.licenseManagement.head_path"
report-section-class="mr-widget-border-top"
......
......@@ -30,7 +30,12 @@ export default {
required: false,
default: null,
},
pipelinePath: {
fullReportPath: {
type: String,
required: false,
default: null,
},
licenseManagementSettingsPath: {
type: String,
required: false,
default: null,
......@@ -64,8 +69,8 @@ export default {
licenseReportStatus() {
return this.checkReportStatus(this.isLoading, this.loadLicenseReportError);
},
licensesTab() {
return this.pipelinePath ? `${this.pipelinePath}/licenses` : null;
showActionButtons() {
return this.licenseManagementSettingsPath !== null || this.fullReportPath !== null;
},
},
watch: {
......@@ -107,15 +112,25 @@ export default {
class="license-report-widget mr-report"
>
<div
v-if="licensesTab"
v-if="showActionButtons"
slot="actionButtons"
class="append-right-default"
>
<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"
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
:size="16"
name="external-link"
......
......@@ -227,7 +227,7 @@ export default {
<a
:href="securityTab"
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>
<icon
......
......@@ -69,5 +69,9 @@ module EE
def license_management_api_url(project)
api_v4_projects_managed_licenses_path(id: project.id)
end
def license_management_settings_path(project)
project_settings_ci_cd_path(project, anchor: 'js-license-management')
end
end
end
......@@ -108,6 +108,14 @@ module EE
expose :can_manage_licenses do |merge_request|
can?(current_user, :admin_software_license_policy, merge_request)
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
# expose_sast_container_data? is deprecated and replaced with expose_container_scanning_data? (#5778)
......
......@@ -6,6 +6,7 @@
- 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
- 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?
#js-tab-security.build-security.tab-pane
......@@ -28,4 +29,5 @@
#js-tab-licenses.tab-pane
#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),
license_management_settings_path: license_management_settings_path,
can_manage_licenses: can?(current_user, :admin_software_license_policy, project).to_s } }
- return unless @project.feature_available?(:license_management)
- 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
%h4
= 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', () => {
const Component = Vue.extend(LicenseManagement);
const apiUrl = `${TEST_HOST}/license_management`;
let vm;
let store;
let actions;
let getters;
const props = {
const defaultState = {
managedLicenses: [approvedLicense, blacklistedLicense],
currentLicenseInModal: licenseReportMock[0],
isLoadingManagedLicenses: true,
};
const defaultGetters = {
isLoading() {
return false;
},
licenseReport() {
return licenseReportMock;
},
licenseSummaryText() {
return 'FOO';
},
};
const defaultProps = {
loadingText: 'LOADING',
errorText: 'ERROR',
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,
};
beforeEach(() => {
actions = {
setAPISettings: jasmine.createSpy('setAPISettings').and.callFake(() => {}),
loadManagedLicenses: jasmine.createSpy('loadManagedLicenses').and.callFake(() => {}),
loadLicenseReport: jasmine.createSpy('loadLicenseReport').and.callFake(() => {}),
};
getters = {
isLoading() {
return false;
},
licenseReport() {
return licenseReportMock;
},
licenseSummaryText() {
return 'FOO';
},
};
store = new Vuex.Store({
state: {
managedLicenses: [approvedLicense, blacklistedLicense],
currentLicenseInModal: licenseReportMock[0],
isLoadingManagedLicenses: true,
},
const defaultActions = {
setAPISettings: () => {},
loadManagedLicenses: () => {},
loadLicenseReport: () => {},
};
const mountComponent = ({
props = defaultProps,
getters = defaultGetters,
state = defaultState,
actions = defaultActions,
} = {}) => {
const store = new Vuex.Store({
state,
getters,
actions,
});
vm = mountComponentWithStore(Component, { props, store });
return mountComponentWithStore(Component, { props, store });
};
beforeEach(() => {
vm = mountComponent();
});
afterEach(() => {
......@@ -62,119 +75,166 @@ describe('License Report MR Widget', () => {
describe('computed', () => {
describe('hasLicenseReportIssues', () => {
it('should be false, if the report is empty', done => {
store.hotUpdate({
getters: {
...getters,
licenseReport() {
return [];
},
it('should be false, if the report is empty', () => {
const getters = {
...defaultGetters,
licenseReport() {
return [];
},
});
return Vue.nextTick().then(() => {
expect(vm.hasLicenseReportIssues).toBe(false);
done();
});
};
vm = mountComponent({ getters });
expect(vm.hasLicenseReportIssues).toBe(false);
});
it('should be true, if the report is not empty', done =>
Vue.nextTick().then(() => {
expect(vm.hasLicenseReportIssues).toBe(true);
done();
}));
it('should be true, if the report is not empty', () => {
expect(vm.hasLicenseReportIssues).toBe(true);
});
});
describe('licensesTab', () => {
it('with the pipelinePath prop', done => {
const pipelinePath = `${TEST_HOST}/path/to/the/pipeline`;
describe('licenseReportStatus', () => {
it('should be `LOADING`, if the report is loading', () => {
const getters = {
...defaultGetters,
isLoading() {
return true;
},
};
vm = mountComponent({ getters });
vm.pipelinePath = pipelinePath;
expect(vm.licenseReportStatus).toBe(LOADING);
});
return Vue.nextTick().then(() => {
expect(vm.licensesTab).toEqual(`${pipelinePath}/licenses`);
done();
});
it('should be `ERROR`, if the report is has an error', () => {
const state = { ...defaultState, loadLicenseReportError: new Error('test') };
vm = mountComponent({ state });
expect(vm.licenseReportStatus).toBe(ERROR);
});
it('without the pipelinePath prop', () => {
expect(vm.licensesTab).toEqual(null);
it('should be `SUCCESS`, if the report is successful', () => {
expect(vm.licenseReportStatus).toBe(SUCCESS);
});
});
describe('licenseReportStatus', () => {
it('should be `LOADING`, if the report is loading', done => {
store.hotUpdate({
getters: {
...getters,
isLoading() {
return true;
},
},
});
return Vue.nextTick().then(() => {
expect(vm.licenseReportStatus).toBe(LOADING);
done();
});
describe('showActionButtons', () => {
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);
});
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 => {
store.replaceState({ ...store.state, loadLicenseReportError: new Error('test') });
return Vue.nextTick().then(() => {
expect(vm.licenseReportStatus).toBe(ERROR);
done();
});
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);
});
});
});
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 =>
Vue.nextTick().then(() => {
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 =>
Vue.nextTick().then(() => {
expect(vm.$el.querySelector('.license-report-widget')).not.toBeNull();
done();
}));
describe('`Manage licenses` button', () => {
const selector = '.js-manage-licenses';
it('should render report widget section', done =>
Vue.nextTick().then(() => {
expect(vm.$el.querySelector('.report-block-container')).not.toBeNull();
done();
}));
it('should be rendered when licenseManagementSettingsPath prop is provided', () => {
const linkEl = vm.$el.querySelector(selector);
expect(linkEl).not.toBeNull();
expect(linkEl.getAttribute('href')).toEqual(defaultProps.licenseManagementSettingsPath);
expect(linkEl.textContent.trim()).toEqual('Manage licenses');
});
it('should render set approval modal', done => {
store.replaceState({ ...store.state });
it('should not be rendered when licenseManagementSettingsPath prop is not provided', () => {
const props = { ...defaultProps, licenseManagementSettingsPath: null };
vm = mountComponent({ props });
return Vue.nextTick().then(() => {
expect(vm.$el.querySelector('#modal-set-license-approval')).not.toBeNull();
done();
const linkEl = vm.$el.querySelector(selector);
expect(linkEl).toBeNull();
});
});
it('should init store after mount', () =>
Vue.nextTick().then(() => {
expect(actions.setAPISettings).toHaveBeenCalledWith(
jasmine.any(Object),
{
apiUrlManageLicenses: apiUrl,
headPath: props.headPath,
basePath: props.basePath,
canManageLicenses: true,
},
undefined,
);
expect(actions.loadManagedLicenses).toHaveBeenCalledWith(
jasmine.any(Object),
undefined,
undefined,
);
expect(actions.loadLicenseReport).toHaveBeenCalledWith(
jasmine.any(Object),
undefined,
undefined,
);
}));
it('should render set approval modal', () => {
expect(vm.$el.querySelector('#modal-set-license-approval')).not.toBeNull();
});
it('should init store after mount', () => {
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(
jasmine.any(Object),
{
apiUrlManageLicenses: apiUrl,
headPath: defaultProps.headPath,
basePath: defaultProps.basePath,
canManageLicenses: true,
},
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
expect(subject.as_json[:dependency_scanning]).to include(:base_path)
end
it 'has license_management data' do
build = create(:ci_build, name: 'license_management', pipeline: pipeline)
allow(merge_request).to receive_messages(
expose_license_management_data?: true,
expose_security_dashboard?: false,
base_has_license_management_data?: true,
base_license_management_artifact: build,
head_license_management_artifact: build
)
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)
describe '#license_management' do
before do
build = create(:ci_build, name: 'license_management', pipeline: pipeline)
allow(merge_request).to receive_messages(
expose_license_management_data?: true,
expose_security_dashboard?: false,
base_has_license_management_data?: true,
base_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[: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
# methods for old artifact are deprecated and replaced with ones for the new name (#5779)
......
......@@ -8745,6 +8745,9 @@ msgstr ""
msgid "ciReport|Loading %{reportName} report"
msgstr ""
msgid "ciReport|Manage licenses"
msgstr ""
msgid "ciReport|Method"
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