Commit 6d94bf37 authored by Alexander Turinske's avatar Alexander Turinske

Add network policy editor empty state

- determine if an environment exists for
  network policy editor
- show empty state if no environment exists
- show loading state if fetching environments
- add tests
parent c83d6e65
<script>
import { GlFormGroup, GlFormInput, GlFormTextarea, GlToggle, GlButton, GlAlert } from '@gitlab/ui';
import {
GlEmptyState,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlLoadingIcon,
GlToggle,
GlButton,
GlAlert,
} from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { removeUnnecessaryDashes } from 'ee/threat_monitoring/utils';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { __, s__ } from '~/locale';
import { EDITOR_MODES, EDITOR_MODE_YAML, PARSING_ERROR_MESSAGE } from '../constants';
import DimDisableContainer from '../dim_disable_container.vue';
import PolicyActionPicker from '../policy_action_picker.vue';
......@@ -24,13 +33,19 @@ import PolicyRuleBuilder from './policy_rule_builder.vue';
export default {
EDITOR_MODES,
i18n: {
toggleLabel: s__('NetworkPolicies|Policy status'),
toggleLabel: s__('SecurityOrchestration|Policy status'),
PARSING_ERROR_MESSAGE,
noEnvironmentDescription: s__(
'SecurityOrchestration|Network Policies can be used to limit which network traffic is allowed between containers inside the cluster.',
),
noEnvironmentButton: __('Learn more'),
},
components: {
GlEmptyState,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlLoadingIcon,
GlToggle,
GlButton,
GlAlert,
......@@ -41,7 +56,7 @@ export default {
PolicyEditorLayout,
DimDisableContainer,
},
inject: ['threatMonitoringPath', 'projectId'],
inject: ['networkDocumentationPath', 'noEnvironmentSvgPath', 'projectId', 'threatMonitoringPath'],
props: {
existingPolicy: {
type: Object,
......@@ -71,6 +86,9 @@ export default {
};
},
computed: {
hasEnvironment() {
return Boolean(this.environments.length);
},
humanizedPolicy() {
return this.policy.error ? null : humanizeNetworkPolicy(this.policy);
},
......@@ -80,7 +98,11 @@ export default {
policyYaml() {
return this.hasParsingError ? '' : toYaml(this.policy);
},
...mapState('threatMonitoring', ['currentEnvironmentId']),
...mapState('threatMonitoring', [
'currentEnvironmentId',
'environments',
'isLoadingEnvironments',
]),
...mapState('networkPolicies', [
'isUpdatingPolicy',
'isRemovingPolicy',
......@@ -159,7 +181,9 @@ export default {
</script>
<template>
<gl-loading-icon v-if="isLoadingEnvironments" size="lg" />
<policy-editor-layout
v-else-if="hasEnvironment"
:is-editing="isEditing"
:is-removing-policy="isRemovingPolicy"
:is-updating-policy="isUpdatingPolicy"
......@@ -255,4 +279,12 @@ export default {
</dim-disable-container>
</template>
</policy-editor-layout>
<gl-empty-state
v-else
:description="$options.i18n.noEnvironmentDescription"
:primary-button-link="networkDocumentationPath"
:primary-button-text="$options.i18n.noEnvironmentButton"
:svg-path="noEnvironmentSvgPath"
title=""
/>
</template>
......@@ -79,10 +79,10 @@ export default {
{{ error }}
</gl-alert>
<header class="gl-pb-5">
<h3>{{ s__('NetworkPolicies|Policy description') }}</h3>
<h3>{{ s__('SecurityOrchestration|Policy description') }}</h3>
</header>
<div class="gl-display-flex">
<gl-form-group :label="s__('NetworkPolicies|Policy type')" label-for="policyType">
<gl-form-group :label="s__('SecurityOrchestration|Policy type')" label-for="policyType">
<gl-form-select
id="policyType"
:value="policyOptions.value"
......
......@@ -20,7 +20,9 @@ export default () => {
environmentsEndpoint,
configureAgentHelpPath,
createAgentHelpPath,
networkDocumentationPath,
networkPoliciesEndpoint,
noEnvironmentSvgPath,
threatMonitoringPath,
policy,
policyType,
......@@ -58,6 +60,8 @@ export default () => {
createAgentHelpPath,
disableScanExecutionUpdate: parseBoolean(disableScanExecutionUpdate),
policyType,
networkDocumentationPath,
noEnvironmentSvgPath,
projectId,
projectPath,
threatMonitoringPath,
......
......@@ -28,6 +28,8 @@ module Projects::Security::PoliciesHelper
create_agent_help_path: help_page_url('user/clusters/agent/index.md', anchor: 'create-an-agent-record-in-gitlab'),
environments_endpoint: project_environments_path(project),
environment_id: environment&.id,
network_documentation_path: help_page_path('user/application_security/threat_monitoring/index.md'),
no_environment_svg_path: image_path('illustrations/monitoring/unable_to_connect.svg'),
policy: policy&.to_json,
policy_type: policy_type,
project_path: project.full_path,
......
import { GlToggle } from '@gitlab/ui';
import { GlEmptyState, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { EDITOR_MODE_YAML } from 'ee/threat_monitoring/components/policy_editor/constants';
import {
RuleDirectionInbound,
......@@ -25,22 +25,33 @@ describe('NetworkPolicyEditor component', () => {
let store;
let wrapper;
const factory = ({ propsData, provide = {}, state, data } = {}) => {
const defaultStore = { threatMonitoring: { environments: [{ id: 1 }], currentEnvironmentId: 1 } };
const factory = ({ propsData, provide = {}, updatedStore = defaultStore, data } = {}) => {
store = createStore();
Object.assign(store.state.threatMonitoring, {
...state,
});
Object.assign(store.state.networkPolicies, {
...state,
store.replaceState({
...store.state,
networkPolicies: {
...store.state.networkPolicies,
...updatedStore.networkPolicies,
},
threatMonitoring: {
...store.state.threatMonitoring,
...updatedStore.threatMonitoring,
},
});
jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve());
wrapper = shallowMountExtended(NetworkPolicyEditor, {
propsData: {
hasEnvironment: true,
...propsData,
},
provide: {
networkDocumentationPath: 'path/to/docs',
noEnvironmentSvgPath: 'path/to/svg',
threatMonitoringPath: '/threat-monitoring',
projectId: '21',
...provide,
......@@ -51,6 +62,8 @@ describe('NetworkPolicyEditor component', () => {
});
};
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPreview = () => wrapper.findComponent(PolicyPreview);
const findAddRuleButton = () => wrapper.findByTestId('add-rule');
const findYAMLParsingAlert = () => wrapper.findByTestId('parsing-alert');
......@@ -92,12 +105,14 @@ describe('NetworkPolicyEditor component', () => {
});
it.each`
component | status | findComponent | state
${'policy alert picker'} | ${'does display'} | ${findPolicyAlertPicker} | ${true}
${'policy name input'} | ${'does display'} | ${findPolicyName} | ${true}
${'add rule button'} | ${'does display'} | ${findAddRuleButton} | ${true}
${'policy preview'} | ${'does display'} | ${findPreview} | ${true}
${'parsing error alert'} | ${'does not display'} | ${findYAMLParsingAlert} | ${false}
component | status | findComponent | state
${'policy alert picker'} | ${'does display'} | ${findPolicyAlertPicker} | ${true}
${'policy name input'} | ${'does display'} | ${findPolicyName} | ${true}
${'add rule button'} | ${'does display'} | ${findAddRuleButton} | ${true}
${'policy preview'} | ${'does display'} | ${findPreview} | ${true}
${'parsing error alert'} | ${'does not display'} | ${findYAMLParsingAlert} | ${false}
${'loading icon'} | ${'does not display'} | ${findLoadingIcon} | ${false}
${'no environment empty state'} | ${'does not display'} | ${findEmptyState} | ${false}
`('$status the $component', async ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
......@@ -140,7 +155,7 @@ describe('NetworkPolicyEditor component', () => {
await findPolicyEditorLayout().vm.$emit('save-policy', EDITOR_MODE_YAML);
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
environmentId: 1,
policy: { manifest: mockL7Manifest },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
......@@ -244,7 +259,7 @@ describe('NetworkPolicyEditor component', () => {
it('creates policy and redirects to a threat monitoring path', async () => {
await findPolicyEditorLayout().vm.$emit('save-policy');
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
environmentId: 1,
policy: { manifest: toYaml(wrapper.vm.policy) },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
......@@ -253,9 +268,7 @@ describe('NetworkPolicyEditor component', () => {
describe('given there is a createPolicy error', () => {
beforeEach(() => {
factory({
state: {
errorUpdatingPolicy: true,
},
updatedStore: { networkPolicies: { errorUpdatingPolicy: true }, ...defaultStore },
});
});
......@@ -289,7 +302,7 @@ describe('NetworkPolicyEditor component', () => {
it('updates existing policy and redirects to a threat monitoring path', async () => {
await findPolicyEditorLayout().vm.$emit('save-policy');
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/updatePolicy', {
environmentId: -1,
environmentId: 1,
policy: { name: 'policy', manifest: toYaml(wrapper.vm.policy) },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
......@@ -298,12 +311,8 @@ describe('NetworkPolicyEditor component', () => {
describe('given there is a updatePolicy error', () => {
beforeEach(() => {
factory({
propsData: {
existingPolicy: { name: 'policy', manifest },
},
state: {
errorUpdatingPolicy: true,
},
propsData: { existingPolicy: { name: 'policy', manifest } },
updatedStore: { networkPolicies: { errorUpdatingPolicy: true }, ...defaultStore },
});
});
......@@ -318,7 +327,7 @@ describe('NetworkPolicyEditor component', () => {
await findPolicyEditorLayout().vm.$emit('remove-policy');
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/deletePolicy', {
environmentId: -1,
environmentId: 1,
policy: { name: 'policy', manifest: toYaml(wrapper.vm.policy) },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
......@@ -329,7 +338,7 @@ describe('NetworkPolicyEditor component', () => {
it('adds a policy annotation on alert addition', async () => {
await modifyPolicyAlert({ isAlertEnabled: true });
expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
environmentId: 1,
policy: {
manifest: expect.stringContaining("app.gitlab.com/alert: 'true'"),
},
......@@ -339,11 +348,42 @@ describe('NetworkPolicyEditor component', () => {
it('removes a policy annotation on alert removal', async () => {
await modifyPolicyAlert({ isAlertEnabled: false });
expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
environmentId: 1,
policy: {
manifest: expect.not.stringContaining("app.gitlab.com/alert: 'true'"),
},
});
});
});
describe('when loading environments', () => {
beforeEach(() => {
factory({
updatedStore: { threatMonitoring: { environments: [], isLoadingEnvironments: true } },
});
});
it.each`
component | status | findComponent | state
${'loading icon'} | ${'does display'} | ${findLoadingIcon} | ${true}
${'policy editor layout'} | ${'does not display'} | ${findPolicyEditorLayout} | ${false}
${'no environment empty state'} | ${'does not display'} | ${findEmptyState} | ${false}
`('$status the $component', ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
});
describe('when no environments are configured', () => {
beforeEach(() => {
factory({ updatedStore: { threatMonitoring: { environments: [] } } });
});
it.each`
component | status | findComponent | state
${'loading icon'} | ${'does display'} | ${findLoadingIcon} | ${false}
${'policy editor layout'} | ${'does not display'} | ${findPolicyEditorLayout} | ${false}
${'no environment empty state'} | ${'does not display'} | ${findEmptyState} | ${true}
`('$status the $component', ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
});
});
......@@ -46,14 +46,17 @@ describe('PolicyEditor component', () => {
beforeEach(factory);
it.each`
component | status | findComponent | state
${'environment picker'} | ${'does display'} | ${findEnvironmentPicker} | ${true}
${'NetworkPolicyEditor component'} | ${'does display'} | ${findNeworkPolicyEditor} | ${true}
${'alert'} | ${'does not display'} | ${findAlert} | ${false}
component | status | findComponent | state
${'environment picker'} | ${'does display'} | ${findEnvironmentPicker} | ${true}
${'alert'} | ${'does not display'} | ${findAlert} | ${false}
`('$status the $component', ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
it('renders the network policy editor component', () => {
expect(findNeworkPolicyEditor().props('existingPolicy')).toBe(null);
});
it('renders the disabled form select', () => {
const formSelect = findFormSelect();
expect(formSelect.exists()).toBe(true);
......
......@@ -44,6 +44,8 @@ RSpec.describe Projects::Security::PoliciesHelper do
configure_agent_help_path: kind_of(String),
create_agent_help_path: kind_of(String),
environments_endpoint: kind_of(String),
network_documentation_path: kind_of(String),
no_environment_svg_path: kind_of(String),
project_path: project.full_path,
project_id: project.id,
threat_monitoring_path: kind_of(String),
......
......@@ -22194,21 +22194,12 @@ msgstr ""
msgid "NetworkPolicies|Policy definition"
msgstr ""
msgid "NetworkPolicies|Policy description"
msgstr ""
msgid "NetworkPolicies|Policy editor"
msgstr ""
msgid "NetworkPolicies|Policy preview"
msgstr ""
msgid "NetworkPolicies|Policy status"
msgstr ""
msgid "NetworkPolicies|Policy type"
msgstr ""
msgid "NetworkPolicies|Rule"
msgstr ""
......@@ -29673,6 +29664,9 @@ msgstr ""
msgid "SecurityOrchestration|Network"
msgstr ""
msgid "SecurityOrchestration|Network Policies can be used to limit which network traffic is allowed between containers inside the cluster."
msgstr ""
msgid "SecurityOrchestration|New policy"
msgstr ""
......@@ -29682,9 +29676,18 @@ msgstr ""
msgid "SecurityOrchestration|Policies"
msgstr ""
msgid "SecurityOrchestration|Policy description"
msgstr ""
msgid "SecurityOrchestration|Policy editor"
msgstr ""
msgid "SecurityOrchestration|Policy status"
msgstr ""
msgid "SecurityOrchestration|Policy type"
msgstr ""
msgid "SecurityOrchestration|Scan Execution"
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