Commit 38873a13 authored by Alexander Turinske's avatar Alexander Turinske

Finish up refactor of policy editor

- ensure everything works properly
- clean up code
- update/add tests
parent e6c03ec6
<script>
import {
fromYaml,
humanizeNetworkPolicy,
removeUnnecessaryDashes,
} from '../policy_editor/network_policy/lib';
import { removeUnnecessaryDashes } from '../../utils';
import { fromYaml, humanizeNetworkPolicy } from '../policy_editor/network_policy/lib';
import PolicyPreview from '../policy_editor/policy_preview.vue';
import BasePolicy from './base_policy.vue';
import PolicyInfoRow from './policy_info_row.vue';
......
<script>
import { GlLink } from '@gitlab/ui';
import { safeLoad } from 'js-yaml';
import { fromYaml } from '../policy_editor/scan_execution_policy/lib';
import BasePolicy from './base_policy.vue';
import PolicyInfoRow from './policy_info_row.vue';
......@@ -18,7 +18,7 @@ export default {
},
computed: {
policy() {
return safeLoad(this.value, { json: true });
return fromYaml(this.value);
},
},
};
......
import { s__, __ } from '~/locale';
export const EditorModeRule = 'rule';
export const EditorModeYAML = 'yaml';
export const EDITOR_MODE_RULE = 'rule';
export const EDITOR_MODE_YAML = 'yaml';
export const PARSING_ERROR_MESSAGE = s__(
'NetworkPolicies|Rule mode is unavailable for this policy. In some cases, we cannot parse the YAML file back into the rules editor.',
);
export const EDITOR_MODES = [
{ value: EditorModeRule, text: s__('NetworkPolicies|Rule mode') },
{ value: EditorModeYAML, text: s__('NetworkPolicies|.yaml mode') },
{ value: EDITOR_MODE_RULE, text: s__('NetworkPolicies|Rule mode') },
{ value: EDITOR_MODE_YAML, text: s__('NetworkPolicies|.yaml mode') },
];
export const POLICY_TYPES = {
......
<script>
import { GlFormGroup, GlFormInput, GlFormTextarea, 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 { EDITOR_MODES, EditorModeYAML, PARSING_ERROR_MESSAGE } from '../constants';
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';
import PolicyAlertPicker from '../policy_alert_picker.vue';
import PolicyEditorLayout from '../policy_editor_layout.vue';
import PolicyPreview from '../policy_preview.vue';
import { removeUnnecessaryDashes } from '../utils';
import {
DEFAULT_NETWORK_POLICY,
RuleTypeEndpoint,
......@@ -128,14 +128,14 @@ export default {
}
},
changeEditorMode(mode) {
if (mode === EditorModeYAML && !this.hasParsingError) {
if (mode === EDITOR_MODE_YAML && !this.hasParsingError) {
this.yamlEditorValue = toYaml(this.policy);
}
},
savePolicy(mode) {
const saveFn = this.isEditing ? this.updatePolicy : this.createPolicy;
const policy = {
manifest: mode === EditorModeYAML ? this.yamlEditorValue : toYaml(this.policy),
manifest: mode === EDITOR_MODE_YAML ? this.yamlEditorValue : toYaml(this.policy),
};
if (this.isEditing) {
policy.name = this.existingPolicy.name;
......@@ -159,6 +159,7 @@ export default {
<template>
<policy-editor-layout
:is-editing="isEditing"
:is-removing-policy="isRemovingPolicy"
:is-updating-policy="isUpdatingPolicy"
:policy-name="policy.name"
:yaml-editor-value="yamlEditorValue"
......
<script>
import { GlButton, GlFormGroup, GlModal, GlModalDirective, GlSegmentedControl } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { DELETE_MODAL_CONFIG, EDITOR_MODES, EditorModeRule, EditorModeYAML } from './constants';
import { DELETE_MODAL_CONFIG, EDITOR_MODES, EDITOR_MODE_RULE, EDITOR_MODE_YAML } from './constants';
export default {
i18n: {
......@@ -18,15 +18,10 @@ export default {
directives: { GlModal: GlModalDirective },
inject: ['threatMonitoringPath'],
props: {
customSaveButtonText: {
type: String,
required: false,
default: '',
},
defaultEditorMode: {
type: String,
required: false,
default: EditorModeRule,
default: EDITOR_MODE_RULE,
},
editorModes: {
type: Array,
......@@ -38,6 +33,11 @@ export default {
required: false,
default: false,
},
isRemovingPolicy: {
type: Boolean,
required: false,
default: false,
},
isUpdatingPolicy: {
type: Boolean,
required: false,
......@@ -64,18 +64,15 @@ export default {
return sprintf(s__('NetworkPolicies|Delete policy: %{policy}'), { policy: this.policyName });
},
saveButtonText() {
if (this.customSaveButtonText) {
return this.customSaveButtonText;
}
return this.isEditing
? s__('NetworkPolicies|Save changes')
: s__('NetworkPolicies|Create policy');
},
shouldShowRuleEditor() {
return this.selectedEditorMode === EditorModeRule;
return this.selectedEditorMode === EDITOR_MODE_RULE;
},
shouldShowYamlEditor() {
return this.selectedEditorMode === EditorModeYAML;
return this.selectedEditorMode === EDITOR_MODE_YAML;
},
},
methods: {
......@@ -89,8 +86,8 @@ export default {
this.selectedEditorMode = mode;
this.$emit('update-editor-mode', mode);
},
updateYaml() {
this.$emit('load-yaml');
updateYaml(manifest) {
this.$emit('update-yaml', manifest);
},
},
};
......@@ -103,7 +100,6 @@ export default {
class="gl-px-5 gl-py-3 gl-mb-0 gl-bg-gray-10 gl-border-b-solid gl-border-b-gray-100 gl-border-b-1"
>
<gl-segmented-control
data-testid="editor-mode"
:options="editorModes"
:checked="selectedEditorMode"
@input="updateEditorMode"
......@@ -111,7 +107,9 @@ export default {
</gl-form-group>
<div class="gl-display-flex gl-sm-flex-direction-column">
<section class="gl-w-full gl-p-5 gl-flex-fill-4 policy-table-left">
<slot v-if="shouldShowRuleEditor" name="rule-editor" data-testid="rule-editor"></slot>
<div v-if="shouldShowRuleEditor" data-testid="rule-editor">
<slot name="rule-editor"></slot>
</div>
<policy-yaml-editor
v-if="shouldShowYamlEditor"
data-testid="policy-yaml-editor"
......@@ -123,6 +121,7 @@ export default {
<section
v-if="shouldShowRuleEditor"
class="gl-w-30p gl-p-5 gl-border-l-gray-100 gl-border-l-1 gl-border-l-solid gl-flex-fill-2"
data-testid="rule-editor-preview"
>
<slot name="rule-editor-preview"></slot>
</section>
......@@ -134,8 +133,11 @@ export default {
data-testid="save-policy"
:loading="isUpdatingPolicy"
@click="savePolicy"
>{{ saveButtonText }}</gl-button
>
<slot name="save-button-text">
{{ saveButtonText }}
</slot>
</gl-button>
<gl-button
v-if="isEditing"
v-gl-modal="'delete-modal'"
......
<script>
import { removeUnnecessaryDashes } from 'ee/threat_monitoring/utils';
import { __ } from '~/locale';
import { EDITOR_MODES, EditorModeYAML } from '../constants';
import { EDITOR_MODES, EDITOR_MODE_YAML } from '../constants';
import PolicyEditorLayout from '../policy_editor_layout.vue';
import { DEFAULT_SCAN_EXECUTION_POLICY, fromYaml } from './lib';
export default {
DEFAULT_EDITOR_MODE: EditorModeYAML,
DEFAULT_EDITOR_MODE: EDITOR_MODE_YAML,
EDITOR_MODES: [EDITOR_MODES[1]],
i18n: {
createMergeRequest: __('Create merge request'),
......@@ -27,28 +28,30 @@ export default {
: fromYaml(DEFAULT_SCAN_EXECUTION_POLICY);
const yamlEditorValue = this.existingPolicy
? this.existingPolicy.manifest
? removeUnnecessaryDashes(this.existingPolicy.manifest)
: DEFAULT_SCAN_EXECUTION_POLICY;
return {
editorMode: EditorModeYAML,
yamlEditorValue,
yamlEditorError: policy.error ? true : null,
isRemovingPolicy: false,
isUpdatingPolicy: false,
policy,
yamlEditorValue,
};
},
computed: {
isCreatingMergeRequest() {
// TODO track the graphql mutation status after #333163 is closed
return false;
},
isEditing() {
return Boolean(this.existingPolicy);
},
},
methods: {
createMergeRequest() {
// TODO call graphql mutation and redirect to merge request after #333163 is closed
removePolicy() {
// TODO call graphql mutation and redirect to merge request after #329422 is closed
},
savePolicy() {
// TODO call graphql mutation and redirect to merge request after #329422 is closed
},
updateYaml(manifest) {
this.yamlEditorValue = manifest;
},
},
};
......@@ -56,13 +59,19 @@ export default {
<template>
<policy-editor-layout
:custom-save-button-text="$options.i18n.createMergeRequest"
:default-editor-mode="$options.DEFAULT_EDITOR_MODE"
:editor-modes="$options.EDITOR_MODES"
:is-editing="isEditing"
:is-updating-policy="isCreatingMergeRequest"
:is-removing-policy="isRemovingPolicy"
:is-updating-policy="isUpdatingPolicy"
:policy-name="policy.name"
:yaml-editor-value="yamlEditorValue"
@save-policy="createMergeRequest"
/>
@remove-policy="removePolicy"
@save-policy="savePolicy"
@update-yaml="updateYaml"
>
<template #save-button-text>
{{ $options.i18n.createMergeRequest }}
</template>
</policy-editor-layout>
</template>
/**
* Removes inital line dashes from a policy YAML that is received from the API, which
* is not required for the user.
* @param {String} manifest the policy from the API request
* @returns {String} the policy without the initial dashes or the initial string
*/
export const removeUnnecessaryDashes = (manifest) => {
return manifest.replace('---\n', '');
};
......@@ -9,3 +9,13 @@ export const getContentWrapperHeight = (contentWrapperClass) => {
const wrapperEl = document.querySelector(contentWrapperClass);
return wrapperEl ? `${wrapperEl.offsetTop}px` : '';
};
/**
* Removes inital line dashes from a policy YAML that is received from the API, which
* is not required for the user.
* @param {String} manifest the policy from the API request
* @returns {String} the policy without the initial dashes or the initial string
*/
export const removeUnnecessaryDashes = (manifest) => {
return manifest.replace('---\n', '');
};
......@@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63585
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/273791
milestone: '14.0'
type: development
group: group::container_security
group: group::container security
default_enabled: false
......@@ -11,7 +11,6 @@ import {
RuleTypeFQDN,
EntityTypes,
fromYaml,
removeUnnecessaryDashes,
buildRule,
toYaml,
} from 'ee/threat_monitoring/components/policy_editor/network_policy/lib';
......@@ -332,15 +331,3 @@ spec:
);
});
});
describe('removeUnnecessaryDashes', () => {
it.each`
input | output
${'---\none'} | ${'one'}
${'two'} | ${'two'}
${'--\nthree'} | ${'--\nthree'}
${'four---\n'} | ${'four'}
`('returns $output when used on $input', ({ input, output }) => {
expect(removeUnnecessaryDashes(input)).toBe(output);
});
});
import { GlModal, GlSegmentedControl } from '@gitlab/ui';
import { EDITOR_MODE_YAML } from 'ee/threat_monitoring/components/policy_editor/constants';
import PolicyEditorLayout from 'ee/threat_monitoring/components/policy_editor/policy_editor_layout.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('PolicyEditorLayout component', () => {
let wrapper;
const threatMonitoringPath = '/threat-monitoring';
const factory = ({ propsData = {} } = {}) => {
wrapper = shallowMountExtended(PolicyEditorLayout, {
propsData: {
...propsData,
},
provide: {
threatMonitoringPath,
},
stubs: { PolicyYamlEditor: true },
});
};
const findDeletePolicyButton = () => wrapper.findByTestId('delete-policy');
const findDeletePolicyModal = () => wrapper.findComponent(GlModal);
const findEditorModeToggle = () => wrapper.findComponent(GlSegmentedControl);
const findYamlModeSection = () => wrapper.findByTestId('policy-yaml-editor');
const findRuleModeSection = () => wrapper.findByTestId('rule-editor');
const findRuleModePreviewSection = () => wrapper.findByTestId('rule-editor-preview');
const findSavePolicyButton = () => wrapper.findByTestId('save-policy');
afterEach(() => {
wrapper.destroy();
});
describe('default behavior', () => {
beforeEach(() => {
factory();
});
it.each`
component | status | findComponent | state
${'editor mode toggle'} | ${'does display'} | ${findEditorModeToggle} | ${true}
${'delete button'} | ${'does not display'} | ${findDeletePolicyButton} | ${false}
`('$status the $component', async ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
it('does display the correct save button text when creating a new policy', () => {
const saveButton = findSavePolicyButton();
expect(saveButton.exists()).toBe(true);
expect(saveButton.text()).toBe('Create policy');
});
it('emits properly with the current mode when the save button is clicked', () => {
findSavePolicyButton().vm.$emit('click');
expect(wrapper.emitted('save-policy')).toStrictEqual([['rule']]);
});
it('mode changes appropriately when new mode is selected', async () => {
expect(findRuleModeSection().exists()).toBe(true);
expect(findYamlModeSection().exists()).toBe(false);
findEditorModeToggle().vm.$emit('input', EDITOR_MODE_YAML);
await wrapper.vm.$nextTick();
expect(findRuleModeSection().exists()).toBe(false);
expect(findYamlModeSection().exists()).toBe(true);
expect(wrapper.emitted('update-editor-mode')).toStrictEqual([[EDITOR_MODE_YAML]]);
});
it('does display custom save button text', () => {
// custom save button text works
const saveButton = findSavePolicyButton();
expect(saveButton.exists()).toBe(true);
expect(saveButton.text()).toBe('Create policy');
});
});
describe('editing a policy', () => {
beforeEach(() => {
factory({ propsData: { isEditing: true } });
});
it('does not emit when the delete button is clicked', () => {
findDeletePolicyButton().vm.$emit('click');
expect(wrapper.emitted('remove-policy')).toStrictEqual(undefined);
});
it('emits properly when the delete modal is closed', () => {
findDeletePolicyModal().vm.$emit('secondary');
expect(wrapper.emitted('remove-policy')).toStrictEqual([[]]);
});
});
describe('rule mode', () => {
beforeEach(() => {
factory();
});
it.each`
component | status | findComponent | state
${'rule mode section'} | ${'does display'} | ${findRuleModeSection} | ${true}
${'rule mode preview section'} | ${'does display'} | ${findRuleModePreviewSection} | ${true}
${'yaml mode section'} | ${'does not display'} | ${findYamlModeSection} | ${false}
`('$status the $component', async ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
});
describe('yaml mode', () => {
beforeEach(() => {
factory({ propsData: { defaultEditorMode: EDITOR_MODE_YAML } });
});
it.each`
component | status | findComponent | state
${'rule mode section'} | ${'does not display'} | ${findRuleModeSection} | ${false}
${'rule mode preview section'} | ${'does not display'} | ${findRuleModePreviewSection} | ${false}
${'yaml mode section'} | ${'does display'} | ${findYamlModeSection} | ${true}
`('$status the $component', async ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
it('emits propertly when yaml is updated', () => {
const newManifest = 'new yaml!';
findYamlModeSection().vm.$emit('input', newManifest);
expect(wrapper.emitted('update-yaml')).toStrictEqual([[newManifest]]);
});
});
});
......@@ -14,7 +14,7 @@ describe('PolicyEditor component', () => {
const findFormSelect = () => wrapper.findComponent(GlFormSelect);
const findNeworkPolicyEditor = () => wrapper.findComponent(NetworkPolicyEditor);
const factory = ({ propsData } = {}) => {
const factory = ({ propsData = {}, provide = {} } = {}) => {
store = createStore();
jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve());
......@@ -25,18 +25,19 @@ describe('PolicyEditor component', () => {
projectId: '21',
...propsData,
},
provide,
store,
stubs: { GlFormSelect },
});
};
beforeEach(factory);
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
beforeEach(factory);
it('renders the environment picker', () => {
expect(findEnvironmentPicker().exists()).toBe(true);
});
......@@ -45,10 +46,23 @@ describe('PolicyEditor component', () => {
const formSelect = findFormSelect();
expect(formSelect.exists()).toBe(true);
expect(formSelect.attributes('value')).toBe(POLICY_TYPES.networkPolicy.value);
expect(formSelect.attributes('disabled')).toBe('true');
});
it('renders the "NetworkPolicyEditor" component', () => {
expect(findNeworkPolicyEditor().exists()).toBe(true);
});
});
describe('with "scanExecutionPolicyUi" feature flag enabled', () => {
beforeEach(() => {
factory({ provide: { glFeatures: { scanExecutionPolicyUi: true } } });
});
it('renders the form select', () => {
const formSelect = findFormSelect();
expect(formSelect.exists()).toBe(true);
expect(formSelect.attributes('disabled')).toBe(undefined);
});
});
});
import {
DEFAULT_SCAN_EXECUTION_POLICY,
fromYaml,
} from 'ee/threat_monitoring/components/policy_editor/scan_execution_policy/lib';
describe('fromYaml', () => {
it('returns policy object', () => {
expect(fromYaml(DEFAULT_SCAN_EXECUTION_POLICY)).toMatchObject({
name: '',
description: '',
enabled: false,
actions: [{ scan: 'dast', site_profile: '', scanner_profile: '' }],
rules: [{ branches: ['main'], type: 'pipeline' }],
});
});
});
import { shallowMount } from '@vue/test-utils';
import PolicyEditorLayout from 'ee/threat_monitoring/components/policy_editor/policy_editor_layout.vue';
import { DEFAULT_SCAN_EXECUTION_POLICY } from 'ee/threat_monitoring/components/policy_editor/scan_execution_policy/lib';
import ScanExecutionPolicyEditor from 'ee/threat_monitoring/components/policy_editor/scan_execution_policy/scan_execution_policy_editor.vue';
import waitForPromises from 'helpers/wait_for_promises';
describe('ScanExecutionPolicyEditor', () => {
let wrapper;
const factory = ({ propsData = {} } = {}) => {
wrapper = shallowMount(ScanExecutionPolicyEditor, {
propsData,
provide: {
threatMonitoringPath: '',
projectId: 1,
},
});
};
const findPolicyEditorLayout = () => wrapper.findComponent(PolicyEditorLayout);
beforeEach(() => {
factory();
});
afterEach(() => {
wrapper.destroy();
});
it('calls the save policy funtion when "save-policy" is emitted', async () => {
const savePolicySpy = jest.spyOn(wrapper.vm, 'savePolicy');
expect(wrapper.vm.savePolicy).toHaveBeenCalledTimes(0);
findPolicyEditorLayout().vm.$emit('save-policy');
await waitForPromises();
expect(wrapper.vm.savePolicy).toHaveBeenCalledTimes(1);
savePolicySpy.mockRestore();
});
it('calls the remove policy funtion when "remove-policy" is emitted', async () => {
const removePolicySpy = jest.spyOn(wrapper.vm, 'removePolicy');
expect(wrapper.vm.removePolicy).toHaveBeenCalledTimes(0);
findPolicyEditorLayout().vm.$emit('remove-policy');
await waitForPromises();
expect(wrapper.vm.removePolicy).toHaveBeenCalledTimes(1);
removePolicySpy.mockRestore();
});
it('updates the policy yaml when "update-yaml" is emitted', async () => {
const updateYamlSpy = jest.spyOn(wrapper.vm, 'updateYaml');
const newManifest = 'new yaml!';
expect(wrapper.vm.updateYaml).toHaveBeenCalledTimes(0);
expect(findPolicyEditorLayout().attributes('yamleditorvalue')).toBe(
DEFAULT_SCAN_EXECUTION_POLICY,
);
findPolicyEditorLayout().vm.$emit('update-yaml', newManifest);
await waitForPromises();
expect(wrapper.vm.updateYaml).toHaveBeenCalledTimes(1);
expect(wrapper.vm.updateYaml).toHaveBeenCalledWith(newManifest);
expect(findPolicyEditorLayout().attributes('yaml-editor-value')).toBe(newManifest);
updateYamlSpy.mockRestore();
});
});
......@@ -181,3 +181,40 @@ export const mockAlertDetails = {
title: 'dropingress',
monitorTool: 'Cilium',
};
export const mockL7Manifest = `apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: limit-inbound-ip
spec:
endpointSelector: {}
ingress:
- toPorts:
- ports:
- port: '80'
protocol: TCP
- port: '443'
protocol: TCP
rules:
http:
- headers:
- 'X-Forwarded-For: 192.168.1.1'
fromEntities:
- cluster`;
export const mockL3Manifest = `apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
description: test description
metadata:
name: test-policy
labels:
app.gitlab.com/proj: '21'
spec:
endpointSelector:
matchLabels:
network-policy.gitlab.com/disabled_by: gitlab
foo: bar
ingress:
- fromEndpoints:
- matchLabels:
foo: bar`;
import { getContentWrapperHeight } from 'ee/threat_monitoring/utils';
import { getContentWrapperHeight, removeUnnecessaryDashes } from 'ee/threat_monitoring/utils';
import { setHTMLFixture } from 'helpers/fixtures';
describe('Threat Monitoring Utils', () => {
......@@ -23,4 +23,16 @@ describe('Threat Monitoring Utils', () => {
expect(getContentWrapperHeight('.does-not-exist')).toBe('');
});
});
describe('removeUnnecessaryDashes', () => {
it.each`
input | output
${'---\none'} | ${'one'}
${'two'} | ${'two'}
${'--\nthree'} | ${'--\nthree'}
${'four---\n'} | ${'four'}
`('returns $output when used on $input', ({ input, output }) => {
expect(removeUnnecessaryDashes(input)).toBe(output);
});
});
});
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