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>
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_MANAGEMENT } from 'ee/vue_shared/license_compliance/store/constants';
import DetectedLicensesTable from './detected_licenses_table.vue';
......@@ -20,6 +29,7 @@ export default {
GlTab,
GlTabs,
GlBadge,
GlAlert,
LicenseManagement,
},
mixins: [glFeatureFlagsMixin()],
......@@ -41,7 +51,7 @@ export default {
computed: {
...mapState(LICENSE_LIST, ['initialized', 'licenses', 'reportInfo', 'listTypes']),
...mapState(LICENSE_MANAGEMENT, ['managedLicenses']),
...mapGetters(LICENSE_LIST, ['isJobSetUp', 'isJobFailed']),
...mapGetters(LICENSE_LIST, ['isJobSetUp', 'isJobFailed', 'hasPolicyViolations']),
hasEmptyState() {
return Boolean(!this.isJobSetUp || this.isJobFailed);
},
......@@ -84,19 +94,29 @@ export default {
/>
<div v-else>
<h2 class="h4">
{{ s__('Licenses|License Compliance') }}
<gl-link :href="documentationPath" class="vertical-align-middle" target="_blank">
<gl-icon name="question" />
</gl-link>
</h2>
<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>
<pipeline-info
v-if="isDetectedProjectTab"
:path="reportInfo.jobPath"
:timestamp="reportInfo.generatedAt"
/>
<template v-else>{{ s__('Licenses|Specified policies in this project') }}</template>
<header class="my-3">
<h2 class="h4 mb-1">
{{ s__('Licenses|License Compliance') }}
<gl-link :href="documentationPath" class="vertical-align-middle" target="_blank">
<gl-icon name="question" />
</gl-link>
</h2>
<pipeline-info
v-if="isDetectedProjectTab"
:path="reportInfo.jobPath"
:timestamp="reportInfo.generatedAt"
/>
<template v-else>{{ s__('Licenses|Specified policies in this project') }}</template>
</header>
<!-- TODO: Remove feature flag -->
<template v-if="hasLicensePolicyList">
......
<script>
import { GlLink, GlSkeletonLoading } from '@gitlab/ui';
import { GlLink, GlSkeletonLoading, GlBadge, GlIcon } from '@gitlab/ui';
import LicenseComponentLinks from './license_component_links.vue';
import { LICENSE_APPROVAL_CLASSIFICATION } from 'ee/vue_shared/license_compliance/constants';
export default {
name: 'LicensesTableRow',
......@@ -8,6 +9,8 @@ export default {
LicenseComponentLinks,
GlLink,
GlSkeletonLoading,
GlBadge,
GlIcon,
},
props: {
license: {
......@@ -21,6 +24,11 @@ export default {
default: true,
},
},
computed: {
isDenied() {
return this.license.classification === LICENSE_APPROVAL_CLASSIFICATION.DENIED;
},
},
};
</script>
......@@ -53,8 +61,15 @@ export default {
<!-- Component -->
<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-content">
<div class="table-mobile-content d-md-flex justify-content-between align-items-center">
<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>
......
import Vue from 'vue';
import Vuex from 'vuex';
import mediator from './plugins/mediator';
import listModule from './modules/list';
import { licenseManagementModule } from 'ee/vue_shared/license_compliance/store/index';
import { LICENSE_LIST } from './constants';
......@@ -14,4 +16,5 @@ export default () =>
[LICENSE_LIST]: listModule(),
[LICENSE_MANAGEMENT]: licenseManagementModule(),
},
plugins: [mediator],
});
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 isJobFailed = state =>
[REPORT_STATUS.jobFailed, REPORT_STATUS.noLicenses, REPORT_STATUS.incomplete].includes(
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';
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
*/
export const LICENSE_APPROVAL_STATUS = {
......@@ -11,6 +11,14 @@ export const LICENSE_APPROVAL_STATUS = {
DENIED: 'blacklisted',
};
/*
* New project licenses endpoint returns 'allowed' & 'denied'
*/
export const LICENSE_APPROVAL_CLASSIFICATION = {
ALLOWED: 'allowed',
DENIED: 'denied',
};
export const LICENSE_APPROVAL_ACTION = {
ALLOW: 'allow',
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';
import Vue from 'vue';
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 { REPORT_STATUS } from 'ee/license_compliance/store/modules/list/constants';
......@@ -19,6 +19,8 @@ import {
blacklistedLicense,
} from 'ee_jest/vue_shared/license_compliance/mock_data';
import { LICENSE_APPROVAL_CLASSIFICATION } from 'ee/vue_shared/license_compliance/constants';
Vue.use(Vuex);
let wrapper;
......@@ -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', () => {
expect(wrapper.find(GlTabs).exists()).toBe(true);
expect(wrapper.find(GlTab).exists()).toBe(true);
......@@ -177,6 +183,24 @@ describe('Project Licenses', () => {
it('renders the pipeline info', () => {
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', () => {
......
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 LicensesTableRow from 'ee/license_compliance/components/licenses_table_row.vue';
import { makeLicense } from './utils';
import { LICENSE_APPROVAL_CLASSIFICATION } from 'ee/vue_shared/license_compliance/constants';
describe('LicensesTableRow component', () => {
let wrapper;
......@@ -97,4 +98,35 @@ describe('LicensesTableRow component', () => {
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 { 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.each`
......@@ -21,4 +22,22 @@ describe('Licenses getters', () => {
).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 ""
msgid "Licenses|Detected in Project"
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"
msgstr ""
......@@ -12106,6 +12109,9 @@ msgstr ""
msgid "Licenses|Policy"
msgstr ""
msgid "Licenses|Policy violation: denied"
msgstr ""
msgid "Licenses|Specified policies in this project"
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