Commit 84ff5886 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '338839-update-policies-empty-state' into 'master'

Create empty states for Policies page

See merge request gitlab-org/gitlab!68506
parents cea3caef acc48523
...@@ -10,6 +10,7 @@ export const EMPTY_STATE_DESCRIPTION = s__( ...@@ -10,6 +10,7 @@ export const EMPTY_STATE_DESCRIPTION = s__(
`ThreatMonitoring|To view this data, ensure you have configured an environment `ThreatMonitoring|To view this data, ensure you have configured an environment
for this project and that at least one threat monitoring feature is enabled. %{linkStart}More information%{linkEnd}`, for this project and that at least one threat monitoring feature is enabled. %{linkStart}More information%{linkEnd}`,
); );
export const NEW_POLICY_BUTTON_TEXT = s__('SecurityOrchestration|New policy');
export const COLORS = { export const COLORS = {
nominal: gray700, nominal: gray700,
......
<script>
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
import { NEW_POLICY_BUTTON_TEXT } from '../constants';
export default {
components: {
GlEmptyState,
},
i18n: {
emptyFilterTitle: s__('SecurityOrchestration|Sorry, your filter produced no results.'),
emptyFilterDescription: s__(
'SecurityOrchestration|To widen your search, change filters above or select a different security policy project.',
),
emptyStateDescription: s__(
'SecurityOrchestration|This project does not contain any security policies.',
),
newPolicyButtonText: NEW_POLICY_BUTTON_TEXT,
},
inject: ['emptyFilterSvgPath', 'emptyListSvgPath', 'newPolicyPath'],
props: {
hasExistingPolicies: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<gl-empty-state
v-if="hasExistingPolicies"
key="empty-filter-state"
data-testid="empty-filter-state"
:svg-path="emptyFilterSvgPath"
:title="$options.i18n.emptyFilterTitle"
:description="$options.i18n.emptyFilterDescription"
/>
<gl-empty-state
v-else
key="empty-list-state"
data-testid="empty-list-state"
:primary-button-link="newPolicyPath"
:primary-button-text="$options.i18n.newPolicyButtonText"
:svg-path="emptyListSvgPath"
title=""
>
<template #description>
<p class="gl-font-weight-bold">
{{ $options.i18n.emptyStateDescription }}
</p>
</template>
</gl-empty-state>
</template>
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import NoEnvironmentEmptyState from '../no_environment_empty_state.vue';
import PoliciesHeader from './policies_header.vue'; import PoliciesHeader from './policies_header.vue';
import PoliciesList from './policies_list.vue'; import PoliciesList from './policies_list.vue';
...@@ -8,7 +7,6 @@ export default { ...@@ -8,7 +7,6 @@ export default {
components: { components: {
PoliciesHeader, PoliciesHeader,
PoliciesList, PoliciesList,
NoEnvironmentEmptyState,
}, },
inject: ['defaultEnvironmentId'], inject: ['defaultEnvironmentId'],
data() { data() {
...@@ -42,9 +40,8 @@ export default { ...@@ -42,9 +40,8 @@ export default {
<template> <template>
<div> <div>
<policies-header @update-policy-list="handleUpdatePolicyList" /> <policies-header @update-policy-list="handleUpdatePolicyList" />
<no-environment-empty-state v-if="!shouldFetchEnvironment" />
<policies-list <policies-list
v-else :has-environment="shouldFetchEnvironment"
:should-update-policy-list="shouldUpdatePolicyList" :should-update-policy-list="shouldUpdatePolicyList"
@update-policy-list="handleUpdatePolicyList" @update-policy-list="handleUpdatePolicyList"
/> />
......
<script> <script>
import { GlAlert, GlSprintf, GlButton } from '@gitlab/ui'; import { GlAlert, GlSprintf, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { NEW_POLICY_BUTTON_TEXT } from '../constants';
import ScanNewPolicyModal from './scan_new_policy_modal.vue'; import ScanNewPolicyModal from './scan_new_policy_modal.vue';
export default { export default {
...@@ -21,7 +22,7 @@ export default { ...@@ -21,7 +22,7 @@ export default {
subtitle: s__( subtitle: s__(
'SecurityOrchestration|Enforce security for this project. %{linkStart}More information.%{linkEnd}', 'SecurityOrchestration|Enforce security for this project. %{linkStart}More information.%{linkEnd}',
), ),
newPolicyButtonText: s__('SecurityOrchestration|New policy'), newPolicyButtonText: NEW_POLICY_BUTTON_TEXT,
editPolicyProjectButtonText: s__('SecurityOrchestration|Edit policy project'), editPolicyProjectButtonText: s__('SecurityOrchestration|Edit policy project'),
}, },
data() { data() {
......
<script> <script>
import { import { GlAlert, GlIcon, GlLink, GlSprintf, GlTable, GlTooltipDirective } from '@gitlab/ui';
GlTable,
GlEmptyState,
GlAlert,
GlSprintf,
GlLink,
GlIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import { PREDEFINED_NETWORK_POLICIES } from 'ee/threat_monitoring/constants'; import { PREDEFINED_NETWORK_POLICIES } from 'ee/threat_monitoring/constants';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -22,6 +14,7 @@ import EnvironmentPicker from '../environment_picker.vue'; ...@@ -22,6 +14,7 @@ import EnvironmentPicker from '../environment_picker.vue';
import PolicyDrawer from '../policy_drawer/policy_drawer.vue'; import PolicyDrawer from '../policy_drawer/policy_drawer.vue';
import PolicyEnvironments from '../policy_environments.vue'; import PolicyEnvironments from '../policy_environments.vue';
import PolicyTypeFilter from '../policy_type_filter.vue'; import PolicyTypeFilter from '../policy_type_filter.vue';
import NoPoliciesEmptyState from './no_policies_empty_state.vue';
const createPolicyFetchError = ({ gqlError, networkError }) => { const createPolicyFetchError = ({ gqlError, networkError }) => {
const error = const error =
...@@ -41,13 +34,13 @@ const getPoliciesWithType = (policies, policyType) => ...@@ -41,13 +34,13 @@ const getPoliciesWithType = (policies, policyType) =>
export default { export default {
components: { components: {
GlTable,
GlEmptyState,
GlAlert, GlAlert,
GlSprintf,
GlLink,
GlIcon, GlIcon,
GlLink,
GlSprintf,
GlTable,
EnvironmentPicker, EnvironmentPicker,
NoPoliciesEmptyState,
PolicyTypeFilter, PolicyTypeFilter,
PolicyDrawer, PolicyDrawer,
PolicyEnvironments, PolicyEnvironments,
...@@ -55,8 +48,13 @@ export default { ...@@ -55,8 +48,13 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
inject: ['projectPath', 'documentationPath', 'newPolicyPath'], inject: ['documentationPath', 'projectPath', 'newPolicyPath'],
props: { props: {
hasEnvironment: {
type: Boolean,
required: false,
default: false,
},
shouldUpdatePolicyList: { shouldUpdatePolicyList: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -74,9 +72,11 @@ export default { ...@@ -74,9 +72,11 @@ export default {
}, },
update(data) { update(data) {
const policies = data?.project?.networkPolicies?.nodes ?? []; const policies = data?.project?.networkPolicies?.nodes ?? [];
const predefined = PREDEFINED_NETWORK_POLICIES.filter( const predefined = this.hasEnvironment
({ name }) => !policies.some((policy) => name === policy.name), ? PREDEFINED_NETWORK_POLICIES.filter(
); ({ name }) => !policies.some((policy) => name === policy.name),
)
: [];
return [...policies, ...predefined]; return [...policies, ...predefined];
}, },
error: createPolicyFetchError, error: createPolicyFetchError,
...@@ -169,6 +169,9 @@ export default { ...@@ -169,6 +169,9 @@ export default {
policyType() { policyType() {
return this.selectedPolicy ? getPolicyType(this.selectedPolicy.yaml) : 'container'; return this.selectedPolicy ? getPolicyType(this.selectedPolicy.yaml) : 'container';
}, },
hasExistingPolicies() {
return !(this.selectedPolicyType === POLICY_TYPE_OPTIONS.ALL.value && !this.policies.length);
},
fields() { fields() {
const environments = { const environments = {
key: 'environments', key: 'environments',
...@@ -232,14 +235,9 @@ export default { ...@@ -232,14 +235,9 @@ export default {
}, },
}, },
i18n: { i18n: {
emptyStateDescription: s__(
`NetworkPolicies|Policies are a specification of how groups of pods are allowed to communicate with each other's network endpoints.`,
),
autodevopsNoticeDescription: s__( autodevopsNoticeDescription: s__(
`NetworkPolicies|If you are using Auto DevOps, your %{monospacedStart}auto-deploy-values.yaml%{monospacedEnd} file will not be updated if you change a policy in this section. Auto DevOps users should make changes by following the %{linkStart}Container Network Policy documentation%{linkEnd}.`, `SecurityOrchestration|If you are using Auto DevOps, your %{monospacedStart}auto-deploy-values.yaml%{monospacedEnd} file will not be updated if you change a policy in this section. Auto DevOps users should make changes by following the %{linkStart}Container Network Policy documentation%{linkEnd}.`,
), ),
emptyStateButton: __('Learn more'),
emptyStateTitle: s__('NetworkPolicies|No policies detected'),
statusEnabled: __('Enabled'), statusEnabled: __('Enabled'),
statusDisabled: __('Disabled'), statusDisabled: __('Disabled'),
}, },
...@@ -317,15 +315,7 @@ export default { ...@@ -317,15 +315,7 @@ export default {
</template> </template>
<template #empty> <template #empty>
<slot name="empty-state"> <no-policies-empty-state :has-existing-policies="hasExistingPolicies" />
<gl-empty-state
ref="tableEmptyState"
:title="$options.i18n.emptyStateTitle"
:description="$options.i18n.emptyStateDescription"
:primary-button-link="documentationFullPath"
:primary-button-text="$options.i18n.emptyStateButton"
/>
</slot>
</template> </template>
</gl-table> </gl-table>
......
...@@ -18,7 +18,8 @@ export default () => { ...@@ -18,7 +18,8 @@ export default () => {
disableSecurityPolicyProject, disableSecurityPolicyProject,
defaultEnvironmentId, defaultEnvironmentId,
environmentsEndpoint, environmentsEndpoint,
emptyStateSvgPath, emptyFilterSvgPath,
emptyListSvgPath,
documentationPath, documentationPath,
newPolicyPath, newPolicyPath,
projectPath, projectPath,
...@@ -37,7 +38,8 @@ export default () => { ...@@ -37,7 +38,8 @@ export default () => {
documentationPath, documentationPath,
newPolicyPath, newPolicyPath,
projectPath, projectPath,
emptyStateSvgPath, emptyFilterSvgPath,
emptyListSvgPath,
defaultEnvironmentId: parseInt(defaultEnvironmentId, 10), defaultEnvironmentId: parseInt(defaultEnvironmentId, 10),
}, },
render(createElement) { render(createElement) {
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
default_environment_id: default_environment_id, default_environment_id: default_environment_id,
disable_security_policy_project: disable_security_policy_project.to_s, disable_security_policy_project: disable_security_policy_project.to_s,
documentation_path: help_page_path('user/application_security/policies/index.md'), documentation_path: help_page_path('user/application_security/policies/index.md'),
empty_state_svg_path: image_path('illustrations/monitoring/unable_to_connect.svg'), empty_filter_svg_path: image_path('illustrations/issues.svg'),
empty_list_svg_path: image_path('illustrations/security-dashboard_empty.svg'),
new_policy_path: new_project_security_policy_path(project), new_policy_path: new_project_security_policy_path(project),
environments_endpoint: project_environments_path(project), environments_endpoint: project_environments_path(project),
project_path: project.full_path } } project_path: project.full_path } }
import NoPoliciesEmptyState from 'ee/threat_monitoring/components/policies/no_policies_empty_state.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('NoPoliciesEmptyState component', () => {
let wrapper;
const findEmptyFilterState = () => wrapper.findByTestId('empty-filter-state');
const findEmptyListState = () => wrapper.findByTestId('empty-list-state');
const factory = (hasExistingPolicies = false) => {
wrapper = shallowMountExtended(NoPoliciesEmptyState, {
propsData: {
hasExistingPolicies,
},
provide: {
emptyFilterSvgPath: 'path/to/filter/svg',
emptyListSvgPath: 'path/to/list/svg',
newPolicyPath: 'path/to/new/policy',
},
});
};
afterEach(() => {
wrapper.destroy();
});
it.each`
title | findComponent | state | factoryFn
${'does display the empty filter state'} | ${findEmptyFilterState} | ${false} | ${factory}
${'does not display the empty list state'} | ${findEmptyListState} | ${true} | ${factory}
${'does not display the empty filter state'} | ${findEmptyFilterState} | ${true} | ${() => factory(true)}
${'does display the empty list state'} | ${findEmptyListState} | ${false} | ${() => factory(true)}
`('$title', async ({ factoryFn, findComponent, state }) => {
factoryFn();
await wrapper.vm.$nextTick();
expect(findComponent().exists()).toBe(state);
});
});
import NoEnvironmentEmptyState from 'ee/threat_monitoring/components/no_environment_empty_state.vue';
import PoliciesApp from 'ee/threat_monitoring/components/policies/policies_app.vue'; import PoliciesApp from 'ee/threat_monitoring/components/policies/policies_app.vue';
import PoliciesHeader from 'ee/threat_monitoring/components/policies/policies_header.vue'; import PoliciesHeader from 'ee/threat_monitoring/components/policies/policies_header.vue';
import PoliciesList from 'ee/threat_monitoring/components/policies/policies_list.vue'; import PoliciesList from 'ee/threat_monitoring/components/policies/policies_list.vue';
...@@ -13,7 +12,6 @@ describe('Policies App', () => { ...@@ -13,7 +12,6 @@ describe('Policies App', () => {
const findPoliciesHeader = () => wrapper.findComponent(PoliciesHeader); const findPoliciesHeader = () => wrapper.findComponent(PoliciesHeader);
const findPoliciesList = () => wrapper.findComponent(PoliciesList); const findPoliciesList = () => wrapper.findComponent(PoliciesList);
const findEmptyState = () => wrapper.findComponent(NoEnvironmentEmptyState);
const createWrapper = ({ provide } = {}) => { const createWrapper = ({ provide } = {}) => {
store = createStore(); store = createStore();
...@@ -49,11 +47,8 @@ describe('Policies App', () => { ...@@ -49,11 +47,8 @@ describe('Policies App', () => {
}); });
it('mounts the policies list component', () => { it('mounts the policies list component', () => {
expect(findPoliciesList().exists()).toBe(true); const policiesList = findPoliciesList();
}); expect(policiesList.props('hasEnvironment')).toBe(true);
it('does not mount the empty state', () => {
expect(findEmptyState().exists()).toBe(false);
}); });
it('fetches the environments when created', async () => { it('fetches the environments when created', async () => {
...@@ -81,16 +76,9 @@ describe('Policies App', () => { ...@@ -81,16 +76,9 @@ describe('Policies App', () => {
createWrapper(); createWrapper();
}); });
it('mounts the policies header component', () => { it('mounts the policies list component', () => {
expect(findPoliciesHeader().exists()).toBe(true); const policiesList = findPoliciesList();
}); expect(policiesList.props('hasEnvironment')).toBe(false);
it('does not mount the policies list component', () => {
expect(findPoliciesList().exists()).toBe(false);
});
it('mounts the empty state', () => {
expect(findEmptyState().exists()).toBe(true);
}); });
it('does not fetch the environments when created', () => { it('does not fetch the environments when created', () => {
......
import { GlAlert, GlButton, GlSprintf } from '@gitlab/ui'; import { GlAlert, GlButton, GlSprintf } from '@gitlab/ui';
import { NEW_POLICY_BUTTON_TEXT } from 'ee/threat_monitoring/components/constants';
import PoliciesHeader from 'ee/threat_monitoring/components/policies/policies_header.vue'; import PoliciesHeader from 'ee/threat_monitoring/components/policies/policies_header.vue';
import ScanNewPolicyModal from 'ee/threat_monitoring/components/policies/scan_new_policy_modal.vue'; import ScanNewPolicyModal from 'ee/threat_monitoring/components/policies/scan_new_policy_modal.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
...@@ -52,7 +53,7 @@ describe('Policies Header Component', () => { ...@@ -52,7 +53,7 @@ describe('Policies Header Component', () => {
}); });
it('displays New policy button with correct text and link', () => { it('displays New policy button with correct text and link', () => {
expect(findNewPolicyButton().text()).toBe('New policy'); expect(findNewPolicyButton().text()).toBe(NEW_POLICY_BUTTON_TEXT);
expect(findNewPolicyButton().attributes('href')).toBe(newPolicyPath); expect(findNewPolicyButton().attributes('href')).toBe(newPolicyPath);
}); });
......
...@@ -65,6 +65,7 @@ describe('PoliciesList component', () => { ...@@ -65,6 +65,7 @@ describe('PoliciesList component', () => {
{ {
propsData: { propsData: {
documentationPath: 'documentation_path', documentationPath: 'documentation_path',
hasEnvironment: true,
newPolicyPath: '/policies/new', newPolicyPath: '/policies/new',
}, },
store, store,
...@@ -84,6 +85,7 @@ describe('PoliciesList component', () => { ...@@ -84,6 +85,7 @@ describe('PoliciesList component', () => {
...GlDrawer.props, ...GlDrawer.props,
}, },
}), }),
NoPoliciesEmptyState: true,
}, },
localVue, localVue,
}, },
...@@ -152,6 +154,10 @@ describe('PoliciesList component', () => { ...@@ -152,6 +154,10 @@ describe('PoliciesList component', () => {
rows = wrapper.findAll('tr'); rows = wrapper.findAll('tr');
}); });
it('does render default network policies', () => {
expect(findPolicyStatusCells()).toHaveLength(5);
});
it('fetches network policies on environment change', async () => { it('fetches network policies on environment change', async () => {
store.dispatch.mockReset(); store.dispatch.mockReset();
await store.commit('threatMonitoring/SET_CURRENT_ENVIRONMENT_ID', 3); await store.commit('threatMonitoring/SET_CURRENT_ENVIRONMENT_ID', 3);
...@@ -305,4 +311,18 @@ describe('PoliciesList component', () => { ...@@ -305,4 +311,18 @@ describe('PoliciesList component', () => {
expect(findAutodevopsAlert().exists()).toBe(true); expect(findAutodevopsAlert().exists()).toBe(true);
}); });
}); });
describe('given no environement', () => {
beforeEach(() => {
mountWrapper({
propsData: {
hasEnvironment: false,
},
});
});
it('does not render default network policies', () => {
expect(findPolicyStatusCells()).toHaveLength(3);
});
});
}); });
...@@ -29646,6 +29646,9 @@ msgstr "" ...@@ -29646,6 +29646,9 @@ msgstr ""
msgid "SecurityOrchestration|Enforce security for this project. %{linkStart}More information.%{linkEnd}" msgid "SecurityOrchestration|Enforce security for this project. %{linkStart}More information.%{linkEnd}"
msgstr "" msgstr ""
msgid "SecurityOrchestration|If you are using Auto DevOps, your %{monospacedStart}auto-deploy-values.yaml%{monospacedEnd} file will not be updated if you change a policy in this section. Auto DevOps users should make changes by following the %{linkStart}Container Network Policy documentation%{linkEnd}."
msgstr ""
msgid "SecurityOrchestration|Network" msgid "SecurityOrchestration|Network"
msgstr "" msgstr ""
...@@ -29676,9 +29679,18 @@ msgstr "" ...@@ -29676,9 +29679,18 @@ msgstr ""
msgid "SecurityOrchestration|Select security project" msgid "SecurityOrchestration|Select security project"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Sorry, your filter produced no results."
msgstr ""
msgid "SecurityOrchestration|There was a problem creating the new security policy" msgid "SecurityOrchestration|There was a problem creating the new security policy"
msgstr "" msgstr ""
msgid "SecurityOrchestration|This project does not contain any security policies."
msgstr ""
msgid "SecurityOrchestration|To widen your search, change filters above or select a different security policy project."
msgstr ""
msgid "SecurityOrchestration|Update scan execution policies" msgid "SecurityOrchestration|Update scan execution policies"
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