Commit 1b238d9b authored by Mark Florian's avatar Mark Florian

Merge branch 'match-displayed-policies' into 'master'

Match displayed policies

See merge request gitlab-org/gitlab!28862
parents 8d15a660 2600e24b
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { GlEmptyState, GlLoadingIcon, GlLink, GlIcon, GlTab, GlTabs, GlBadge } from '@gitlab/ui'; import {
GlEmptyState,
GlLoadingIcon,
GlLink,
GlIcon,
GlTab,
GlTabs,
GlBadge,
GlAlert,
} from '@gitlab/ui';
import { LICENSE_LIST } from '../store/constants'; import { LICENSE_LIST } from '../store/constants';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_compliance/store/constants'; import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_compliance/store/constants';
import DetectedLicensesTable from './detected_licenses_table.vue'; import DetectedLicensesTable from './detected_licenses_table.vue';
...@@ -20,6 +29,7 @@ export default { ...@@ -20,6 +29,7 @@ export default {
GlTab, GlTab,
GlTabs, GlTabs,
GlBadge, GlBadge,
GlAlert,
LicenseManagement, LicenseManagement,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
...@@ -41,7 +51,7 @@ export default { ...@@ -41,7 +51,7 @@ export default {
computed: { computed: {
...mapState(LICENSE_LIST, ['initialized', 'licenses', 'reportInfo', 'listTypes']), ...mapState(LICENSE_LIST, ['initialized', 'licenses', 'reportInfo', 'listTypes']),
...mapState(LICENSE_MANAGEMENT, ['managedLicenses']), ...mapState(LICENSE_MANAGEMENT, ['managedLicenses']),
...mapGetters(LICENSE_LIST, ['isJobSetUp', 'isJobFailed']), ...mapGetters(LICENSE_LIST, ['isJobSetUp', 'isJobFailed', 'hasPolicyViolations']),
hasEmptyState() { hasEmptyState() {
return Boolean(!this.isJobSetUp || this.isJobFailed); return Boolean(!this.isJobSetUp || this.isJobFailed);
}, },
...@@ -84,7 +94,16 @@ export default { ...@@ -84,7 +94,16 @@ export default {
/> />
<div v-else> <div v-else>
<h2 class="h4"> <gl-alert v-if="hasPolicyViolations" class="mt-2" variant="warning" :dismissible="false">
{{
s__(
"Licenses|Detected licenses that are out-of-compliance with the project's assigned policies",
)
}}
</gl-alert>
<header class="my-3">
<h2 class="h4 mb-1">
{{ s__('Licenses|License Compliance') }} {{ s__('Licenses|License Compliance') }}
<gl-link :href="documentationPath" class="vertical-align-middle" target="_blank"> <gl-link :href="documentationPath" class="vertical-align-middle" target="_blank">
<gl-icon name="question" /> <gl-icon name="question" />
...@@ -97,6 +116,7 @@ export default { ...@@ -97,6 +116,7 @@ export default {
:timestamp="reportInfo.generatedAt" :timestamp="reportInfo.generatedAt"
/> />
<template v-else>{{ s__('Licenses|Specified policies in this project') }}</template> <template v-else>{{ s__('Licenses|Specified policies in this project') }}</template>
</header>
<!-- TODO: Remove feature flag --> <!-- TODO: Remove feature flag -->
<template v-if="hasLicensePolicyList"> <template v-if="hasLicensePolicyList">
......
<script> <script>
import { GlLink, GlSkeletonLoading } from '@gitlab/ui'; import { GlLink, GlSkeletonLoading, GlBadge, GlIcon } from '@gitlab/ui';
import LicenseComponentLinks from './license_component_links.vue'; import LicenseComponentLinks from './license_component_links.vue';
import { LICENSE_APPROVAL_CLASSIFICATION } from 'ee/vue_shared/license_compliance/constants';
export default { export default {
name: 'LicensesTableRow', name: 'LicensesTableRow',
...@@ -8,6 +9,8 @@ export default { ...@@ -8,6 +9,8 @@ export default {
LicenseComponentLinks, LicenseComponentLinks,
GlLink, GlLink,
GlSkeletonLoading, GlSkeletonLoading,
GlBadge,
GlIcon,
}, },
props: { props: {
license: { license: {
...@@ -21,6 +24,11 @@ export default { ...@@ -21,6 +24,11 @@ export default {
default: true, default: true,
}, },
}, },
computed: {
isDenied() {
return this.license.classification === LICENSE_APPROVAL_CLASSIFICATION.DENIED;
},
},
}; };
</script> </script>
...@@ -53,8 +61,15 @@ export default { ...@@ -53,8 +61,15 @@ export default {
<!-- Component --> <!-- Component -->
<div class="table-section section-70 section-wrap pr-md-3"> <div class="table-section section-70 section-wrap pr-md-3">
<div class="table-mobile-header" role="rowheader">{{ s__('Licenses|Component') }}</div> <div class="table-mobile-header" role="rowheader">{{ s__('Licenses|Component') }}</div>
<div class="table-mobile-content"> <div class="table-mobile-content d-md-flex justify-content-between align-items-center">
<license-component-links :components="license.components" :title="license.name" /> <license-component-links :components="license.components" :title="license.name" />
<div v-if="isDenied" class="d-inline-block">
<!-- This badge usage will be simplified in https://gitlab.com/gitlab-org/gitlab/-/issues/213789 -->
<gl-badge variant="warning" class="gl-alert-warning d-flex align-items-center">
<gl-icon name="warning" :size="16" class="pr-1" />
<span>{{ s__('Licenses|Policy violation: denied') }}</span>
</gl-badge>
</div>
</div> </div>
</div> </div>
</div> </div>
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import mediator from './plugins/mediator';
import listModule from './modules/list'; import listModule from './modules/list';
import { licenseManagementModule } from 'ee/vue_shared/license_compliance/store/index'; import { licenseManagementModule } from 'ee/vue_shared/license_compliance/store/index';
import { LICENSE_LIST } from './constants'; import { LICENSE_LIST } from './constants';
...@@ -14,4 +16,5 @@ export default () => ...@@ -14,4 +16,5 @@ export default () =>
[LICENSE_LIST]: listModule(), [LICENSE_LIST]: listModule(),
[LICENSE_MANAGEMENT]: licenseManagementModule(), [LICENSE_MANAGEMENT]: licenseManagementModule(),
}, },
plugins: [mediator],
}); });
import { REPORT_STATUS } from './constants'; import { REPORT_STATUS } from './constants';
import { LICENSE_APPROVAL_CLASSIFICATION } from 'ee/vue_shared/license_compliance/constants';
export const isJobSetUp = state => state.reportInfo.status !== REPORT_STATUS.jobNotSetUp; export const isJobSetUp = state => state.reportInfo.status !== REPORT_STATUS.jobNotSetUp;
export const isJobFailed = state => export const isJobFailed = state =>
[REPORT_STATUS.jobFailed, REPORT_STATUS.noLicenses, REPORT_STATUS.incomplete].includes( [REPORT_STATUS.jobFailed, REPORT_STATUS.noLicenses, REPORT_STATUS.incomplete].includes(
state.reportInfo.status, state.reportInfo.status,
); );
export const hasPolicyViolations = state => {
return state.licenses.some(
license => license.classification === LICENSE_APPROVAL_CLASSIFICATION.DENIED,
);
};
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_compliance/store/constants';
import * as licenseMangementMutationTypes from 'ee/vue_shared/license_compliance/store/mutation_types';
import { LICENSE_LIST } from '../constants';
export default store => {
store.subscribe(({ type }) => {
switch (type) {
case `${LICENSE_MANAGEMENT}/${licenseMangementMutationTypes.RECEIVE_SET_LICENSE_APPROVAL}`:
case `${LICENSE_MANAGEMENT}/${licenseMangementMutationTypes.RECEIVE_DELETE_LICENSE}`:
store.dispatch(`${LICENSE_LIST}/fetchLicenses`);
break;
default:
}
});
};
...@@ -3,7 +3,7 @@ import { __, s__ } from '~/locale'; ...@@ -3,7 +3,7 @@ import { __, s__ } from '~/locale';
import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants'; import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
/* /*
* Endpoint still returns 'approved' & 'blacklisted' * Legacy endpoint still returns 'approved' & 'blacklisted'
* even though we adopted 'allowed' & 'denied' in the UI * even though we adopted 'allowed' & 'denied' in the UI
*/ */
export const LICENSE_APPROVAL_STATUS = { export const LICENSE_APPROVAL_STATUS = {
...@@ -11,6 +11,14 @@ export const LICENSE_APPROVAL_STATUS = { ...@@ -11,6 +11,14 @@ export const LICENSE_APPROVAL_STATUS = {
DENIED: 'blacklisted', DENIED: 'blacklisted',
}; };
/*
* New project licenses endpoint returns 'allowed' & 'denied'
*/
export const LICENSE_APPROVAL_CLASSIFICATION = {
ALLOWED: 'allowed',
DENIED: 'denied',
};
export const LICENSE_APPROVAL_ACTION = { export const LICENSE_APPROVAL_ACTION = {
ALLOW: 'allow', ALLOW: 'allow',
DENY: 'deny', DENY: 'deny',
......
---
title: Show policy violations in license compliance
merge_request: 28862
author:
type: added
...@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { GlEmptyState, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui'; import { GlEmptyState, GlLoadingIcon, GlTab, GlTabs, GlAlert } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { REPORT_STATUS } from 'ee/license_compliance/store/modules/list/constants'; import { REPORT_STATUS } from 'ee/license_compliance/store/modules/list/constants';
...@@ -19,6 +19,8 @@ import { ...@@ -19,6 +19,8 @@ import {
blacklistedLicense, blacklistedLicense,
} from 'ee_jest/vue_shared/license_compliance/mock_data'; } from 'ee_jest/vue_shared/license_compliance/mock_data';
import { LICENSE_APPROVAL_CLASSIFICATION } from 'ee/vue_shared/license_compliance/constants';
Vue.use(Vuex); Vue.use(Vuex);
let wrapper; let wrapper;
...@@ -160,6 +162,10 @@ describe('Project Licenses', () => { ...@@ -160,6 +162,10 @@ describe('Project Licenses', () => {
}); });
}); });
it('does not render a policy violations alert', () => {
expect(wrapper.find(GlAlert).exists()).toBe(false);
});
it('renders a "Detected in project" tab and a "Policies" tab', () => { it('renders a "Detected in project" tab and a "Policies" tab', () => {
expect(wrapper.find(GlTabs).exists()).toBe(true); expect(wrapper.find(GlTabs).exists()).toBe(true);
expect(wrapper.find(GlTab).exists()).toBe(true); expect(wrapper.find(GlTab).exists()).toBe(true);
...@@ -177,6 +183,24 @@ describe('Project Licenses', () => { ...@@ -177,6 +183,24 @@ describe('Project Licenses', () => {
it('renders the pipeline info', () => { it('renders the pipeline info', () => {
expect(wrapper.find(PipelineInfo).exists()).toBe(true); expect(wrapper.find(PipelineInfo).exists()).toBe(true);
}); });
describe('when there are policy violations', () => {
beforeEach(() => {
createComponent({
state: {
initialized: true,
licenses: [{ classification: LICENSE_APPROVAL_CLASSIFICATION.DENIED }],
},
});
});
it('renders a policy violations alert', () => {
expect(wrapper.find(GlAlert).exists()).toBe(true);
expect(wrapper.find(GlAlert).text()).toContain(
"Detected licenses that are out-of-compliance with the project's assigned policies",
);
});
});
}); });
describe('when licensePolicyList feature flag is disabled', () => { describe('when licensePolicyList feature flag is disabled', () => {
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlLink, GlSkeletonLoading } from '@gitlab/ui'; import { GlLink, GlSkeletonLoading, GlBadge } from '@gitlab/ui';
import LicenseComponentLinks from 'ee/license_compliance/components/license_component_links.vue'; import LicenseComponentLinks from 'ee/license_compliance/components/license_component_links.vue';
import LicensesTableRow from 'ee/license_compliance/components/licenses_table_row.vue'; import LicensesTableRow from 'ee/license_compliance/components/licenses_table_row.vue';
import { makeLicense } from './utils'; import { makeLicense } from './utils';
import { LICENSE_APPROVAL_CLASSIFICATION } from 'ee/vue_shared/license_compliance/constants';
describe('LicensesTableRow component', () => { describe('LicensesTableRow component', () => {
let wrapper; let wrapper;
...@@ -97,4 +98,35 @@ describe('LicensesTableRow component', () => { ...@@ -97,4 +98,35 @@ describe('LicensesTableRow component', () => {
expect(nameSection.find(GlLink).exists()).toBe(false); expect(nameSection.find(GlLink).exists()).toBe(false);
}); });
}); });
describe('when a license has a denied policy violation', () => {
beforeEach(() => {
license = makeLicense({ classification: LICENSE_APPROVAL_CLASSIFICATION.DENIED });
factory({
isLoading: false,
license,
});
});
it('shows the policy violation badge', () => {
expect(wrapper.find(GlBadge).exists()).toBe(true);
expect(wrapper.find(GlBadge).text()).toContain('Policy violation: denied');
});
});
describe('when a license is allowed', () => {
beforeEach(() => {
license = makeLicense({ classification: LICENSE_APPROVAL_CLASSIFICATION.ALLOWED });
factory({
isLoading: false,
license,
});
});
it('does not show the policy violation badge', () => {
expect(wrapper.find(GlBadge).exists()).toBe(false);
});
});
}); });
import * as getters from 'ee/license_compliance/store/modules/list/getters'; import * as getters from 'ee/license_compliance/store/modules/list/getters';
import { REPORT_STATUS } from 'ee/license_compliance/store/modules/list/constants'; import { REPORT_STATUS } from 'ee/license_compliance/store/modules/list/constants';
import { LICENSE_APPROVAL_CLASSIFICATION } from 'ee/vue_shared/license_compliance/constants';
describe('Licenses getters', () => { describe('Licenses getters', () => {
describe.each` describe.each`
...@@ -21,4 +22,22 @@ describe('Licenses getters', () => { ...@@ -21,4 +22,22 @@ describe('Licenses getters', () => {
).toBe(outcome); ).toBe(outcome);
}); });
}); });
describe('hasPolicyViolations', () => {
it('returns true when there are policy violations', () => {
expect(
getters.hasPolicyViolations({
licenses: [{ classification: LICENSE_APPROVAL_CLASSIFICATION.DENIED }, {}],
}),
).toBe(true);
});
it('returns false when there are policy violations', () => {
expect(
getters.hasPolicyViolations({
licenses: [{ classification: LICENSE_APPROVAL_CLASSIFICATION.ALLOWED }, {}],
}),
).toBe(false);
});
});
}); });
import createStore from 'ee/license_compliance/store/index';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_compliance/store/constants';
import { LICENSE_LIST } from 'ee/license_compliance/store/constants';
import * as licenseMangementMutationTypes from 'ee/vue_shared/license_compliance/store/mutation_types';
describe('mediator', () => {
let store;
beforeEach(() => {
store = createStore();
jest.spyOn(store, 'dispatch').mockImplementation();
});
it('triggers fetching of detected licenses after a license policy is added or edited', () => {
store.commit(
`${LICENSE_MANAGEMENT}/${licenseMangementMutationTypes.RECEIVE_SET_LICENSE_APPROVAL}`,
);
expect(store.dispatch).toHaveBeenCalledWith(`${LICENSE_LIST}/fetchLicenses`);
});
it('triggers fetching of detected licenses after a license policy is deleted', () => {
store.commit(`${LICENSE_MANAGEMENT}/${licenseMangementMutationTypes.RECEIVE_DELETE_LICENSE}`);
expect(store.dispatch).toHaveBeenCalledWith(`${LICENSE_LIST}/fetchLicenses`);
});
});
...@@ -12085,6 +12085,9 @@ msgstr "" ...@@ -12085,6 +12085,9 @@ msgstr ""
msgid "Licenses|Detected in Project" msgid "Licenses|Detected in Project"
msgstr "" msgstr ""
msgid "Licenses|Detected licenses that are out-of-compliance with the project's assigned policies"
msgstr ""
msgid "Licenses|Displays licenses detected in the project, based on the %{linkStart}latest successful%{linkEnd} scan" msgid "Licenses|Displays licenses detected in the project, based on the %{linkStart}latest successful%{linkEnd} scan"
msgstr "" msgstr ""
...@@ -12106,6 +12109,9 @@ msgstr "" ...@@ -12106,6 +12109,9 @@ msgstr ""
msgid "Licenses|Policy" msgid "Licenses|Policy"
msgstr "" msgstr ""
msgid "Licenses|Policy violation: denied"
msgstr ""
msgid "Licenses|Specified policies in this project" msgid "Licenses|Specified policies in this project"
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