Commit d11e5d63 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '340339-policy-editor-performance' into 'master'

Fix policy editor performance

See merge request gitlab-org/gitlab!70024
parents af0ea9b1 d44df987
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { isValidEnvironmentId } from '../../utils';
import PoliciesHeader from './policies_header.vue'; import PoliciesHeader from './policies_header.vue';
import PoliciesList from './policies_list.vue'; import PoliciesList from './policies_list.vue';
...@@ -15,7 +16,7 @@ export default { ...@@ -15,7 +16,7 @@ export default {
// An invalid default environment id means there there are no available // An invalid default environment id means there there are no available
// environments, therefore infrastructure cannot be set up. A valid default // environments, therefore infrastructure cannot be set up. A valid default
// environment id only means that infrastructure *might* be set up. // environment id only means that infrastructure *might* be set up.
shouldFetchEnvironment: this.isValidEnvironmentId(this.defaultEnvironmentId), shouldFetchEnvironment: isValidEnvironmentId(this.defaultEnvironmentId),
shouldUpdatePolicyList: false, shouldUpdatePolicyList: false,
}; };
}, },
...@@ -27,10 +28,6 @@ export default { ...@@ -27,10 +28,6 @@ export default {
}, },
methods: { methods: {
...mapActions('threatMonitoring', ['fetchEnvironments', 'setCurrentEnvironmentId']), ...mapActions('threatMonitoring', ['fetchEnvironments', 'setCurrentEnvironmentId']),
isValidEnvironmentId(id) {
return Number.isInteger(id) && id >= 0;
},
handleUpdatePolicyList(val) { handleUpdatePolicyList(val) {
this.shouldUpdatePolicyList = val; this.shouldUpdatePolicyList = val;
}, },
......
...@@ -4,7 +4,6 @@ import { ...@@ -4,7 +4,6 @@ import {
GlFormGroup, GlFormGroup,
GlFormInput, GlFormInput,
GlFormTextarea, GlFormTextarea,
GlLoadingIcon,
GlToggle, GlToggle,
GlButton, GlButton,
GlAlert, GlAlert,
...@@ -48,13 +47,15 @@ export default { ...@@ -48,13 +47,15 @@ export default {
noEnvironmentButton: __('Learn more'), noEnvironmentButton: __('Learn more'),
policyPreview: s__('SecurityOrchestration|Policy preview'), policyPreview: s__('SecurityOrchestration|Policy preview'),
rules: s__('SecurityOrchestration|Rules'), rules: s__('SecurityOrchestration|Rules'),
saveButtonTooltip: s__(
'NetworkPolicies|Network policy can be created after the environment is loaded successfully.',
),
}, },
components: { components: {
GlEmptyState, GlEmptyState,
GlFormGroup, GlFormGroup,
GlFormInput, GlFormInput,
GlFormTextarea, GlFormTextarea,
GlLoadingIcon,
GlToggle, GlToggle,
GlButton, GlButton,
GlAlert, GlAlert,
...@@ -65,7 +66,13 @@ export default { ...@@ -65,7 +66,13 @@ export default {
PolicyEditorLayout, PolicyEditorLayout,
DimDisableContainer, DimDisableContainer,
}, },
inject: ['networkDocumentationPath', 'noEnvironmentSvgPath', 'projectId', 'threatMonitoringPath'], inject: [
'hasEnvironment',
'networkDocumentationPath',
'noEnvironmentSvgPath',
'projectId',
'threatMonitoringPath',
],
props: { props: {
existingPolicy: { existingPolicy: {
type: Object, type: Object,
...@@ -95,8 +102,8 @@ export default { ...@@ -95,8 +102,8 @@ export default {
}; };
}, },
computed: { computed: {
hasEnvironment() { customSaveTooltipText() {
return Boolean(this.environments.length); return !this.retrievedEnvironments ? this.$options.i18n.saveButtonTooltip : '';
}, },
humanizedPolicy() { humanizedPolicy() {
return this.policy.error ? null : humanizeNetworkPolicy(this.policy); return this.policy.error ? null : humanizeNetworkPolicy(this.policy);
...@@ -110,6 +117,9 @@ export default { ...@@ -110,6 +117,9 @@ export default {
policyYaml() { policyYaml() {
return this.hasParsingError ? '' : toYaml(this.policy); return this.hasParsingError ? '' : toYaml(this.policy);
}, },
retrievedEnvironments() {
return !this.isLoadingEnvironments && Boolean(this.environments.length);
},
...mapState('threatMonitoring', [ ...mapState('threatMonitoring', [
'currentEnvironmentId', 'currentEnvironmentId',
'environments', 'environments',
...@@ -193,9 +203,11 @@ export default { ...@@ -193,9 +203,11 @@ export default {
</script> </script>
<template> <template>
<gl-loading-icon v-if="isLoadingEnvironments" size="lg" />
<policy-editor-layout <policy-editor-layout
v-else-if="hasEnvironment" v-if="hasEnvironment"
:custom-save-tooltip-text="customSaveTooltipText"
:disable-tooltip="retrievedEnvironments"
:disable-update="!retrievedEnvironments"
:is-editing="isEditing" :is-editing="isEditing"
:is-removing-policy="isRemovingPolicy" :is-removing-policy="isRemovingPolicy"
:is-updating-policy="isUpdatingPolicy" :is-updating-policy="isUpdatingPolicy"
......
<script> <script>
import { GlButton, GlFormGroup, GlModal, GlModalDirective, GlSegmentedControl } from '@gitlab/ui'; import {
GlButton,
GlFormGroup,
GlModal,
GlModalDirective,
GlSegmentedControl,
GlTooltipDirective,
} from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { DELETE_MODAL_CONFIG, EDITOR_MODES, EDITOR_MODE_RULE, EDITOR_MODE_YAML } from './constants'; import { DELETE_MODAL_CONFIG, EDITOR_MODES, EDITOR_MODE_RULE, EDITOR_MODE_YAML } from './constants';
...@@ -15,14 +22,29 @@ export default { ...@@ -15,14 +22,29 @@ export default {
PolicyYamlEditor: () => PolicyYamlEditor: () =>
import(/* webpackChunkName: 'policy_yaml_editor' */ '../policy_yaml_editor.vue'), import(/* webpackChunkName: 'policy_yaml_editor' */ '../policy_yaml_editor.vue'),
}, },
directives: { GlModal: GlModalDirective }, directives: { GlModal: GlModalDirective, GlTooltip: GlTooltipDirective },
inject: ['threatMonitoringPath'], inject: ['threatMonitoringPath'],
props: { props: {
customSaveButtonText: {
type: String,
required: false,
default: '',
},
customSaveTooltipText: {
type: String,
required: false,
default: '',
},
defaultEditorMode: { defaultEditorMode: {
type: String, type: String,
required: false, required: false,
default: EDITOR_MODE_RULE, default: EDITOR_MODE_RULE,
}, },
disableTooltip: {
type: Boolean,
required: false,
default: true,
},
disableUpdate: { disableUpdate: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -68,10 +90,16 @@ export default { ...@@ -68,10 +90,16 @@ export default {
deleteModalTitle() { deleteModalTitle() {
return sprintf(s__('NetworkPolicies|Delete policy: %{policy}'), { policy: this.policyName }); return sprintf(s__('NetworkPolicies|Delete policy: %{policy}'), { policy: this.policyName });
}, },
saveTooltipText() {
return this.customSaveTooltipText || this.saveButtonText;
},
saveButtonText() { saveButtonText() {
return this.isEditing return (
? s__('NetworkPolicies|Save changes') this.customSaveButtonText ||
: s__('NetworkPolicies|Create policy'); (this.isEditing
? s__('NetworkPolicies|Save changes')
: s__('NetworkPolicies|Create policy'))
);
}, },
shouldShowRuleEditor() { shouldShowRuleEditor() {
return this.selectedEditorMode === EDITOR_MODE_RULE; return this.selectedEditorMode === EDITOR_MODE_RULE;
...@@ -132,18 +160,23 @@ export default { ...@@ -132,18 +160,23 @@ export default {
</section> </section>
</div> </div>
</div> </div>
<gl-button <span
type="submit" v-gl-tooltip.hover.focus="{ disabled: disableTooltip }"
variant="success" class="gl-pt-2"
data-testid="save-policy" :title="saveTooltipText"
:loading="isUpdatingPolicy" data-testid="save-policy-tooltip"
:disabled="disableUpdate"
@click="savePolicy"
> >
<slot name="save-button-text"> <gl-button
type="submit"
variant="success"
data-testid="save-policy"
:loading="isUpdatingPolicy"
:disabled="disableUpdate"
@click="savePolicy"
>
{{ saveButtonText }} {{ saveButtonText }}
</slot> </gl-button>
</gl-button> </span>
<gl-button <gl-button
v-if="isEditing" v-if="isEditing"
v-gl-modal="'delete-modal'" v-gl-modal="'delete-modal'"
......
...@@ -110,6 +110,7 @@ export default { ...@@ -110,6 +110,7 @@ export default {
<template> <template>
<policy-editor-layout <policy-editor-layout
:custom-save-button-text="$options.i18n.createMergeRequest"
:default-editor-mode="$options.DEFAULT_EDITOR_MODE" :default-editor-mode="$options.DEFAULT_EDITOR_MODE"
:disable-update="disableScanExecutionUpdate" :disable-update="disableScanExecutionUpdate"
:editor-modes="$options.EDITOR_MODES" :editor-modes="$options.EDITOR_MODES"
...@@ -121,9 +122,5 @@ export default { ...@@ -121,9 +122,5 @@ export default {
@remove-policy="handleModifyPolicy($options.SECURITY_POLICY_ACTIONS.REMOVE)" @remove-policy="handleModifyPolicy($options.SECURITY_POLICY_ACTIONS.REMOVE)"
@save-policy="handleModifyPolicy()" @save-policy="handleModifyPolicy()"
@update-yaml="updateYaml" @update-yaml="updateYaml"
> />
<template #save-button-text>
{{ $options.i18n.createMergeRequest }}
</template>
</policy-editor-layout>
</template> </template>
...@@ -4,7 +4,7 @@ import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_ ...@@ -4,7 +4,7 @@ import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_
import PolicyEditorApp from './components/policy_editor/policy_editor.vue'; import PolicyEditorApp from './components/policy_editor/policy_editor.vue';
import { DEFAULT_ASSIGNED_POLICY_PROJECT } from './constants'; import { DEFAULT_ASSIGNED_POLICY_PROJECT } from './constants';
import createStore from './store'; import createStore from './store';
import { gqClient } from './utils'; import { gqClient, isValidEnvironmentId } from './utils';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -16,6 +16,7 @@ export default () => { ...@@ -16,6 +16,7 @@ export default () => {
const el = document.querySelector('#js-policy-builder-app'); const el = document.querySelector('#js-policy-builder-app');
const { const {
assignedPolicyProject, assignedPolicyProject,
defaultEnvironmentId,
disableScanExecutionUpdate, disableScanExecutionUpdate,
environmentsEndpoint, environmentsEndpoint,
configureAgentHelpPath, configureAgentHelpPath,
...@@ -64,6 +65,7 @@ export default () => { ...@@ -64,6 +65,7 @@ export default () => {
noEnvironmentSvgPath, noEnvironmentSvgPath,
projectId, projectId,
projectPath, projectPath,
hasEnvironment: isValidEnvironmentId(parseInt(defaultEnvironmentId, 10)),
threatMonitoringPath, threatMonitoringPath,
}, },
store, store,
......
...@@ -28,6 +28,15 @@ export const getPolicyType = (typeName = '') => { ...@@ -28,6 +28,15 @@ export const getPolicyType = (typeName = '') => {
return null; return null;
}; };
/**
* Checks if an environment id is valid
* @param {Number} id environment id
* @returns {Boolean}
*/
export const isValidEnvironmentId = (id) => {
return Number.isInteger(id) && id >= 0;
};
/** /**
* Removes inital line dashes from a policy YAML that is received from the API, which * Removes inital line dashes from a policy YAML that is received from the API, which
* is not required for the user. * is not required for the user.
......
...@@ -22,6 +22,7 @@ module Projects::Security::PoliciesHelper ...@@ -22,6 +22,7 @@ module Projects::Security::PoliciesHelper
{ {
assigned_policy_project: assigned_policy_project(project).to_json, assigned_policy_project: assigned_policy_project(project).to_json,
default_environment_id: project.default_environment&.id || -1,
disable_scan_execution_update: disable_scan_execution_update.to_s, disable_scan_execution_update: disable_scan_execution_update.to_s,
network_policies_endpoint: project_security_network_policies_path(project), network_policies_endpoint: project_security_network_policies_path(project),
configure_agent_help_path: help_page_url('user/clusters/agent/repository.html'), configure_agent_help_path: help_page_url('user/clusters/agent/repository.html'),
......
import { GlEmptyState, GlLoadingIcon, GlToggle } from '@gitlab/ui'; import { GlEmptyState, GlToggle } from '@gitlab/ui';
import { EDITOR_MODE_YAML } from 'ee/threat_monitoring/components/policy_editor/constants'; import { EDITOR_MODE_YAML } from 'ee/threat_monitoring/components/policy_editor/constants';
import DimDisableContainer from 'ee/threat_monitoring/components/policy_editor/dim_disable_container.vue'; import DimDisableContainer from 'ee/threat_monitoring/components/policy_editor/dim_disable_container.vue';
import { import {
...@@ -46,10 +46,10 @@ describe('NetworkPolicyEditor component', () => { ...@@ -46,10 +46,10 @@ describe('NetworkPolicyEditor component', () => {
wrapper = shallowMountExtended(NetworkPolicyEditor, { wrapper = shallowMountExtended(NetworkPolicyEditor, {
propsData: { propsData: {
hasEnvironment: true,
...propsData, ...propsData,
}, },
provide: { provide: {
hasEnvironment: true,
networkDocumentationPath: 'path/to/docs', networkDocumentationPath: 'path/to/docs',
noEnvironmentSvgPath: 'path/to/svg', noEnvironmentSvgPath: 'path/to/svg',
threatMonitoringPath: '/threat-monitoring', threatMonitoringPath: '/threat-monitoring',
...@@ -62,7 +62,6 @@ describe('NetworkPolicyEditor component', () => { ...@@ -62,7 +62,6 @@ describe('NetworkPolicyEditor component', () => {
}; };
const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPreview = () => wrapper.findComponent(PolicyPreview); const findPreview = () => wrapper.findComponent(PolicyPreview);
const findAddRuleButton = () => wrapper.findByTestId('add-rule'); const findAddRuleButton = () => wrapper.findByTestId('add-rule');
const findYAMLParsingAlert = () => wrapper.findByTestId('parsing-alert'); const findYAMLParsingAlert = () => wrapper.findByTestId('parsing-alert');
...@@ -94,6 +93,13 @@ describe('NetworkPolicyEditor component', () => { ...@@ -94,6 +93,13 @@ describe('NetworkPolicyEditor component', () => {
expect(policyEnableToggle.props('label')).toBe(NetworkPolicyEditor.i18n.toggleLabel); expect(policyEnableToggle.props('label')).toBe(NetworkPolicyEditor.i18n.toggleLabel);
}); });
it('disables the tooltip and enables the save button', () => {
expect(findPolicyEditorLayout().props()).toMatchObject({
disableTooltip: true,
disableUpdate: false,
});
});
it('renders a default rule with label', () => { it('renders a default rule with label', () => {
expect(wrapper.findAllComponents(PolicyRuleBuilder)).toHaveLength(1); expect(wrapper.findAllComponents(PolicyRuleBuilder)).toHaveLength(1);
expect(findPolicyRuleBuilder().exists()).toBe(true); expect(findPolicyRuleBuilder().exists()).toBe(true);
...@@ -110,7 +116,6 @@ describe('NetworkPolicyEditor component', () => { ...@@ -110,7 +116,6 @@ describe('NetworkPolicyEditor component', () => {
${'add rule button'} | ${'does display'} | ${findAddRuleButton} | ${true} ${'add rule button'} | ${'does display'} | ${findAddRuleButton} | ${true}
${'policy preview'} | ${'does display'} | ${findPreview} | ${true} ${'policy preview'} | ${'does display'} | ${findPreview} | ${true}
${'parsing error alert'} | ${'does not display'} | ${findYAMLParsingAlert} | ${false} ${'parsing error alert'} | ${'does not display'} | ${findYAMLParsingAlert} | ${false}
${'loading icon'} | ${'does not display'} | ${findLoadingIcon} | ${false}
${'no environment empty state'} | ${'does not display'} | ${findEmptyState} | ${false} ${'no environment empty state'} | ${'does not display'} | ${findEmptyState} | ${false}
`('$status the $component', async ({ findComponent, state }) => { `('$status the $component', async ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state); expect(findComponent().exists()).toBe(state);
...@@ -356,24 +361,29 @@ describe('NetworkPolicyEditor component', () => { ...@@ -356,24 +361,29 @@ describe('NetworkPolicyEditor component', () => {
updatedStore: { threatMonitoring: { environments: [], isLoadingEnvironments: true } }, updatedStore: { threatMonitoring: { environments: [], isLoadingEnvironments: true } },
}); });
}); });
it.each`
component | status | findComponent | state it('does not display the "no environment" empty state', () => {
${'loading icon'} | ${'does display'} | ${findLoadingIcon} | ${true} expect(findEmptyState().exists()).toBe(false);
${'policy editor layout'} | ${'does not display'} | ${findPolicyEditorLayout} | ${false} });
${'no environment empty state'} | ${'does not display'} | ${findEmptyState} | ${false}
`('$status the $component', ({ findComponent, state }) => { it('displays the "PolicyEditorLayout" component enables the tooltip and disables the save button', () => {
expect(findComponent().exists()).toBe(state); expect(findPolicyEditorLayout().props()).toMatchObject({
disableTooltip: false,
disableUpdate: true,
});
}); });
}); });
describe('when no environments are configured', () => { describe('when no environments are configured', () => {
beforeEach(() => { beforeEach(() => {
factory({ updatedStore: { threatMonitoring: { environments: [] } } }); factory({
provide: { hasEnvironment: false },
updatedStore: { threatMonitoring: { environments: [] } },
});
}); });
it.each` it.each`
component | status | findComponent | state component | status | findComponent | state
${'loading icon'} | ${'does display'} | ${findLoadingIcon} | ${false}
${'policy editor layout'} | ${'does not display'} | ${findPolicyEditorLayout} | ${false} ${'policy editor layout'} | ${'does not display'} | ${findPolicyEditorLayout} | ${false}
${'no environment empty state'} | ${'does not display'} | ${findEmptyState} | ${true} ${'no environment empty state'} | ${'does not display'} | ${findEmptyState} | ${true}
`('$status the $component', ({ findComponent, state }) => { `('$status the $component', ({ findComponent, state }) => {
......
...@@ -5,10 +5,15 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; ...@@ -5,10 +5,15 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('PolicyEditorLayout component', () => { describe('PolicyEditorLayout component', () => {
let wrapper; let wrapper;
let glTooltipDirectiveMock;
const threatMonitoringPath = '/threat-monitoring'; const threatMonitoringPath = '/threat-monitoring';
const factory = ({ propsData = {} } = {}) => { const factory = ({ propsData = {} } = {}) => {
glTooltipDirectiveMock = jest.fn();
wrapper = shallowMountExtended(PolicyEditorLayout, { wrapper = shallowMountExtended(PolicyEditorLayout, {
directives: {
GlTooltip: glTooltipDirectiveMock,
},
propsData: { propsData: {
...propsData, ...propsData,
}, },
...@@ -44,6 +49,10 @@ describe('PolicyEditorLayout component', () => { ...@@ -44,6 +49,10 @@ describe('PolicyEditorLayout component', () => {
expect(findComponent().exists()).toBe(state); expect(findComponent().exists()).toBe(state);
}); });
it('disables the save button tooltip', async () => {
expect(glTooltipDirectiveMock.mock.calls[0][1].value.disabled).toBe(true);
});
it('does display the correct save button text when creating a new policy', () => { it('does display the correct save button text when creating a new policy', () => {
const saveButton = findSavePolicyButton(); const saveButton = findSavePolicyButton();
expect(saveButton.exists()).toBe(true); expect(saveButton.exists()).toBe(true);
...@@ -124,11 +133,25 @@ describe('PolicyEditorLayout component', () => { ...@@ -124,11 +133,25 @@ describe('PolicyEditorLayout component', () => {
}); });
}); });
describe('disabled actions', () => { describe('custom behavior', () => {
it('disables the save button', async () => { it('displays the custom save button text when it is passed in', async () => {
const customSaveButtonText = 'Custom Text';
factory({ propsData: { customSaveButtonText } });
expect(findSavePolicyButton().exists()).toBe(true);
expect(findSavePolicyButton().text()).toBe(customSaveButtonText);
});
it('disables the save button when "disableUpdate" is true', async () => {
factory({ propsData: { disableUpdate: true } }); factory({ propsData: { disableUpdate: true } });
expect(findSavePolicyButton().exists()).toBe(true); expect(findSavePolicyButton().exists()).toBe(true);
expect(findSavePolicyButton().attributes('disabled')).toBe('true'); expect(findSavePolicyButton().attributes('disabled')).toBe('true');
}); });
it('enables the save button tooltip when "disableTooltip" is false', async () => {
const customSaveTooltipText = 'Custom Test';
factory({ propsData: { customSaveTooltipText, disableTooltip: false } });
expect(glTooltipDirectiveMock.mock.calls[0][1].value.disabled).toBe(false);
expect(glTooltipDirectiveMock.mock.calls[0][0].title).toBe(customSaveTooltipText);
});
}); });
}); });
...@@ -3,6 +3,7 @@ import { POLICY_TYPE_COMPONENT_OPTIONS } from 'ee/threat_monitoring/components/c ...@@ -3,6 +3,7 @@ import { POLICY_TYPE_COMPONENT_OPTIONS } from 'ee/threat_monitoring/components/c
import { import {
getContentWrapperHeight, getContentWrapperHeight,
getPolicyType, getPolicyType,
isValidEnvironmentId,
removeUnnecessaryDashes, removeUnnecessaryDashes,
} from 'ee/threat_monitoring/utils'; } from 'ee/threat_monitoring/utils';
import { setHTMLFixture } from 'helpers/fixtures'; import { setHTMLFixture } from 'helpers/fixtures';
...@@ -44,6 +45,19 @@ describe('Threat Monitoring Utils', () => { ...@@ -44,6 +45,19 @@ describe('Threat Monitoring Utils', () => {
}); });
}); });
describe('isValidEnvironmentId', () => {
it.each`
input | output
${-1} | ${false}
${undefined} | ${false}
${'0'} | ${false}
${0} | ${true}
${1} | ${true}
`('returns $output when used on $input', ({ input, output }) => {
expect(isValidEnvironmentId(input)).toBe(output);
});
});
describe('removeUnnecessaryDashes', () => { describe('removeUnnecessaryDashes', () => {
it.each` it.each`
input | output input | output
......
...@@ -39,6 +39,7 @@ RSpec.describe Projects::Security::PoliciesHelper do ...@@ -39,6 +39,7 @@ RSpec.describe Projects::Security::PoliciesHelper do
let(:base_data) do let(:base_data) do
{ {
assigned_policy_project: "null", assigned_policy_project: "null",
default_environment_id: -1,
disable_scan_execution_update: "false", disable_scan_execution_update: "false",
network_policies_endpoint: kind_of(String), network_policies_endpoint: kind_of(String),
configure_agent_help_path: kind_of(String), configure_agent_help_path: kind_of(String),
...@@ -84,7 +85,7 @@ RSpec.describe Projects::Security::PoliciesHelper do ...@@ -84,7 +85,7 @@ RSpec.describe Projects::Security::PoliciesHelper do
) )
end end
it { is_expected.to match(base_data) } it { is_expected.to match(base_data.merge(default_environment_id: project.default_environment.id)) }
end end
end end
end end
...@@ -22337,6 +22337,9 @@ msgstr "" ...@@ -22337,6 +22337,9 @@ msgstr ""
msgid "NetworkPolicies|Network Policies can be used to limit which network traffic is allowed between containers inside the cluster." msgid "NetworkPolicies|Network Policies can be used to limit which network traffic is allowed between containers inside the cluster."
msgstr "" msgstr ""
msgid "NetworkPolicies|Network policy can be created after the environment is loaded successfully."
msgstr ""
msgid "NetworkPolicies|Network traffic" msgid "NetworkPolicies|Network traffic"
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