Commit ea9ff9a4 authored by ap4y's avatar ap4y

Add NetworkPolicyList component

This commit introduces a new component that will be used on the new
policy management tab. Environment dropdown was extracted from the
filters into a separate component and re-used in the new list
component.
parent d1b31894
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
export default {
components: {
GlFormGroup,
GlDropdown,
GlDropdownItem,
},
computed: {
...mapState('threatMonitoring', ['environments', 'currentEnvironmentId']),
...mapGetters('threatMonitoring', ['currentEnvironmentName', 'canChangeEnvironment']),
},
methods: {
...mapActions('threatMonitoring', ['setCurrentEnvironmentId']),
},
environmentFilterId: 'threat-monitoring-environment-filter',
};
</script>
<template>
<gl-form-group
:label="s__('ThreatMonitoring|Environment')"
label-size="sm"
:label-for="$options.environmentFilterId"
class="col-sm-6 col-md-4 col-lg-3 col-xl-2"
>
<gl-dropdown
:id="$options.environmentFilterId"
ref="environmentsDropdown"
class="mb-0 d-flex"
toggle-class="d-flex justify-content-between text-truncate"
:text="currentEnvironmentName"
:disabled="!canChangeEnvironment"
>
<gl-dropdown-item
v-for="environment in environments"
:key="environment.id"
ref="environmentsDropdownItem"
@click="setCurrentEnvironmentId(environment.id)"
>{{ environment.name }}</gl-dropdown-item
>
</gl-dropdown>
</gl-form-group>
</template>
<script>
import { mapState } from 'vuex';
import { GlTable, GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
import { getTimeago } from '~/lib/utils/datetime_utility';
import { setUrlFragment } from '~/lib/utils/url_utility';
import EnvironmentPicker from './environment_picker.vue';
export default {
components: {
GlTable,
GlEmptyState,
EnvironmentPicker,
},
props: {
documentationPath: {
type: String,
required: true,
},
},
computed: {
...mapState('networkPolicies', ['policies', 'isLoadingPolicies']),
documentationFullPath() {
return setUrlFragment(this.documentationPath, 'container-network-policy');
},
},
methods: {
getTimeAgoString(creationTimestamp) {
return getTimeago().format(creationTimestamp);
},
},
fields: [
{ key: 'name', label: s__('NetworkPolicies|Name'), thClass: 'w-75 font-weight-bold' },
{ key: 'status', label: s__('NetworkPolicies|Status'), thClass: 'font-weight-bold' },
{
key: 'creationTimestamp',
label: s__('NetworkPolicies|Last modified'),
thClass: 'font-weight-bold',
},
],
emptyStateDescription: s__(
`NetworkPolicies|Policies are a specification of how groups of pods are allowed to communicate with each other network endpoints.`,
),
};
</script>
<template>
<div>
<div class="pt-3 px-3 bg-gray-light">
<div class="row">
<environment-picker ref="environmentsPicker" />
</div>
</div>
<gl-table
ref="policiesTable"
:busy="isLoadingPolicies"
:items="policies"
:fields="$options.fields"
head-variant="white"
stacked="md"
thead-class="gl-text-gray-900 border-bottom"
tbody-class="gl-text-gray-900"
show-empty
>
<template #cell(status)>
{{ s__('NetworkPolicies|Enabled') }}
</template>
<template #cell(creationTimestamp)="value">
{{ getTimeAgoString(value.item.creationTimestamp) }}
</template>
<template #empty>
<slot name="emptyState">
<gl-empty-state
ref="tableEmptyState"
:title="s__('NetworkPolicies|No policies detected')"
:description="$options.emptyStateDescription"
:primary-button-link="documentationFullPath"
:primary-button-text="__('Learn More')"
/>
</slot>
</template>
</gl-table>
</div>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
import { GlFormGroup } from '@gitlab/ui';
import { timeRanges, defaultTimeRange } from '~/vue_shared/constants';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import EnvironmentPicker from './environment_picker.vue';
export default {
name: 'ThreatMonitoringFilters',
components: {
GlFormGroup,
GlDropdown,
GlDropdownItem,
DateTimePicker,
EnvironmentPicker,
},
data() {
return {
......@@ -19,56 +19,22 @@ export default {
};
},
computed: {
...mapState('threatMonitoring', [
'environments',
'currentEnvironmentId',
'isLoadingEnvironments',
'isLoadingWafStatistics',
]),
...mapGetters('threatMonitoring', ['currentEnvironmentName']),
isDisabled() {
return (
this.isLoadingEnvironments || this.isLoadingWafStatistics || this.environments.length === 0
);
},
...mapGetters('threatMonitoring', ['canChangeEnvironment']),
},
methods: {
...mapActions('threatMonitoring', ['setCurrentEnvironmentId', 'setCurrentTimeWindow']),
...mapActions('threatMonitoring', ['setCurrentTimeWindow']),
onDateTimePickerInput(timeRange) {
this.selectedTimeRange = timeRange;
this.setCurrentTimeWindow(timeRange);
},
},
environmentFilterId: 'threat-monitoring-environment-filter',
};
</script>
<template>
<div class="pt-3 px-3 bg-gray-light">
<div class="row">
<gl-form-group
:label="s__('ThreatMonitoring|Environment')"
label-size="sm"
:label-for="$options.environmentFilterId"
class="col-sm-6 col-md-4 col-lg-3 col-xl-2"
>
<gl-dropdown
:id="$options.environmentFilterId"
ref="environmentsDropdown"
class="mb-0 d-flex"
toggle-class="d-flex justify-content-between text-truncate"
:text="currentEnvironmentName"
:disabled="isDisabled"
>
<gl-dropdown-item
v-for="environment in environments"
:key="environment.id"
ref="environmentsDropdownItem"
@click="setCurrentEnvironmentId(environment.id)"
>{{ environment.name }}</gl-dropdown-item
>
</gl-dropdown>
</gl-form-group>
<environment-picker />
<gl-form-group
:label="s__('ThreatMonitoring|Show last')"
......@@ -81,7 +47,7 @@ export default {
:custom-enabled="false"
:value="selectedTimeRange"
:options="timeRanges"
:disabled="isDisabled"
:disabled="!canChangeEnvironment"
@input="onDateTimePickerInput"
/>
</gl-form-group>
......
/* eslint-disable import/prefer-default-export */
import { INVALID_CURRENT_ENVIRONMENT_NAME } from '../../../constants';
export const currentEnvironmentName = ({ currentEnvironmentId, environments }) => {
const environment = environments.find(({ id }) => id === currentEnvironmentId);
return environment ? environment.name : INVALID_CURRENT_ENVIRONMENT_NAME;
};
export const canChangeEnvironment = ({
isLoadingEnvironments,
isLoadingWafStatistics,
isLoadingNetworkPolicyStatistics,
environments,
}) =>
!isLoadingEnvironments &&
!isLoadingWafStatistics &&
!isLoadingNetworkPolicyStatistics &&
environments.length > 0;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NetworkPolicyList component given there is a default environment with no data to display shows the table empty state 1`] = `
<section
class="row empty-state text-center"
>
<div
class="col-12"
>
<!---->
</div>
<div
class="col-12"
>
<div
class="text-content gl-mx-auto gl-my-0 gl-p-5"
>
<h1
class="h4"
>
No policies detected
</h1>
<p>
Policies are a specification of how groups of pods are allowed to communicate with each other network endpoints.
</p>
<div>
<a
class="btn btn-success btn-md gl-button"
href="documentation_path#container-network-policy"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Learn More
</span>
</a>
<!---->
</div>
</div>
</div>
</section>
`;
exports[`NetworkPolicyList component renders policies table 1`] = `
<table
aria-busy="false"
aria-colcount="3"
aria-describedby="__BVID__39__caption_"
class="table b-table gl-table b-table-stacked-md"
id="__BVID__39"
role="table"
>
<!---->
<!---->
<thead
class="thead-white gl-text-gray-900 border-bottom"
role="rowgroup"
>
<!---->
<tr
class=""
role="row"
>
<th
aria-colindex="1"
class="w-75 font-weight-bold"
role="columnheader"
scope="col"
>
Name
</th>
<th
aria-colindex="2"
class="font-weight-bold"
role="columnheader"
scope="col"
>
Status
</th>
<th
aria-colindex="3"
class="font-weight-bold"
role="columnheader"
scope="col"
>
Last modified
</th>
</tr>
</thead>
<tbody
class="gl-text-gray-900"
role="rowgroup"
>
<!---->
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
data-label="Name"
role="cell"
>
<div>
policy
</div>
</td>
<td
aria-colindex="2"
class=""
data-label="Status"
role="cell"
>
<div>
Enabled
</div>
</td>
<td
aria-colindex="3"
class=""
data-label="Last modified"
role="cell"
>
<div>
just now
</div>
</td>
</tr>
<!---->
<!---->
</tbody>
<!---->
</table>
`;
import { shallowMount } from '@vue/test-utils';
import createStore from 'ee/threat_monitoring/store';
import EnvironmentPicker from 'ee/threat_monitoring/components/environment_picker.vue';
import { INVALID_CURRENT_ENVIRONMENT_NAME } from 'ee/threat_monitoring/constants';
import { mockEnvironmentsResponse } from '../mock_data';
const mockEnvironments = mockEnvironmentsResponse.environments;
describe('EnvironmentPicker component', () => {
let store;
let wrapper;
const factory = state => {
store = createStore();
Object.assign(store.state.threatMonitoring, state);
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(EnvironmentPicker, {
store,
});
};
const findEnvironmentsDropdown = () => wrapper.find({ ref: 'environmentsDropdown' });
const findEnvironmentsDropdownItems = () => wrapper.findAll({ ref: 'environmentsDropdownItem' });
afterEach(() => {
wrapper.destroy();
});
describe('the environments dropdown', () => {
describe('given there are no environments', () => {
beforeEach(() => {
factory();
});
it('has text set to the INVALID_CURRENT_ENVIRONMENT_NAME', () => {
expect(findEnvironmentsDropdown().attributes().text).toBe(INVALID_CURRENT_ENVIRONMENT_NAME);
});
it('has no dropdown items', () => {
expect(findEnvironmentsDropdownItems()).toHaveLength(0);
});
});
describe('given there are environments', () => {
const currentEnvironment = mockEnvironments[1];
beforeEach(() => {
factory({
environments: mockEnvironments,
currentEnvironmentId: currentEnvironment.id,
});
});
it('is not disabled', () => {
expect(findEnvironmentsDropdown().attributes().disabled).toBe(undefined);
});
it('has text set to the current environment', () => {
expect(findEnvironmentsDropdown().attributes().text).toBe(currentEnvironment.name);
});
it('has dropdown items for each environment', () => {
const dropdownItems = findEnvironmentsDropdownItems();
mockEnvironments.forEach((environment, i) => {
const dropdownItem = dropdownItems.at(i);
expect(dropdownItem.text()).toBe(environment.name);
dropdownItem.vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith(
'threatMonitoring/setCurrentEnvironmentId',
environment.id,
);
});
});
});
});
describe.each`
context | isLoadingEnvironments | isLoadingWafStatistics | isLoadingNetworkPolicyStatistics | environments
${'environments are loading'} | ${true} | ${false} | ${false} | ${mockEnvironments}
${'WAF statistics are loading'} | ${false} | ${true} | ${false} | ${mockEnvironments}
${'NetPol statistics are loading'} | ${false} | ${false} | ${true} | ${mockEnvironments}
${'there are no environments'} | ${false} | ${false} | ${false} | ${[]}
`(
'given $context',
({
isLoadingEnvironments,
isLoadingWafStatistics,
isLoadingNetworkPolicyStatistics,
environments,
}) => {
beforeEach(() => {
factory({
environments,
isLoadingEnvironments,
isLoadingWafStatistics,
isLoadingNetworkPolicyStatistics,
});
return wrapper.vm.$nextTick();
});
it('disables the environments dropdown', () => {
expect(findEnvironmentsDropdown().attributes('disabled')).toBe('true');
});
},
);
});
import { mount } from '@vue/test-utils';
import createStore from 'ee/threat_monitoring/store';
import NetworkPolicyList from 'ee/threat_monitoring/components/network_policy_list.vue';
import { GlTable } from '@gitlab/ui';
import { mockPoliciesResponse } from '../mock_data';
describe('NetworkPolicyList component', () => {
let store;
let wrapper;
const factory = ({ propsData, state } = {}) => {
store = createStore();
Object.assign(store.state.networkPolicies, {
isLoadingPolicies: false,
policies: mockPoliciesResponse,
...state,
});
wrapper = mount(NetworkPolicyList, {
propsData: {
documentationPath: 'documentation_path',
...propsData,
},
store,
});
};
const findEnvironmentsPicker = () => wrapper.find({ ref: 'environmentsPicker' });
const findPoliciesTable = () => wrapper.find(GlTable);
const findTableEmptyState = () => wrapper.find({ ref: 'tableEmptyState' });
beforeEach(() => {
factory({});
});
afterEach(() => {
wrapper.destroy();
});
it('renders EnvironmentPicker', () => {
expect(findEnvironmentsPicker().exists()).toBe(true);
});
it('renders policies table', () => {
expect(findPoliciesTable().element).toMatchSnapshot();
});
describe('given there is a default environment with no data to display', () => {
beforeEach(() => {
factory({
state: {
policies: [],
},
});
});
it('shows the table empty state', () => {
expect(findTableEmptyState().element).toMatchSnapshot();
});
});
});
import { shallowMount } from '@vue/test-utils';
import createStore from 'ee/threat_monitoring/store';
import ThreatMonitoringFilters from 'ee/threat_monitoring/components/threat_monitoring_filters.vue';
import { INVALID_CURRENT_ENVIRONMENT_NAME } from 'ee/threat_monitoring/constants';
import EnvironmentPicker from 'ee/threat_monitoring/components/environment_picker.vue';
import { mockEnvironmentsResponse } from '../mock_data';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import { timeRanges, defaultTimeRange } from '~/vue_shared/constants';
......@@ -23,61 +23,20 @@ describe('ThreatMonitoringFilters component', () => {
});
};
const findEnvironmentsDropdown = () => wrapper.find({ ref: 'environmentsDropdown' });
const findEnvironmentsDropdownItems = () => wrapper.findAll({ ref: 'environmentsDropdownItem' });
const findEnvironmentsPicker = () => wrapper.find(EnvironmentPicker);
const findShowLastDropdown = () => wrapper.find(DateTimePicker);
afterEach(() => {
wrapper.destroy();
});
describe('the environments dropdown', () => {
describe('given there are no environments', () => {
beforeEach(() => {
factory();
});
it('has text set to the INVALID_CURRENT_ENVIRONMENT_NAME', () => {
expect(findEnvironmentsDropdown().attributes().text).toBe(INVALID_CURRENT_ENVIRONMENT_NAME);
});
it('has no dropdown items', () => {
expect(findEnvironmentsDropdownItems()).toHaveLength(0);
});
describe('the environments picker', () => {
beforeEach(() => {
factory();
});
describe('given there are environments', () => {
const currentEnvironment = mockEnvironments[1];
beforeEach(() => {
factory({
environments: mockEnvironments,
currentEnvironmentId: currentEnvironment.id,
});
});
it('is not disabled', () => {
expect(findEnvironmentsDropdown().attributes().disabled).toBe(undefined);
});
it('has text set to the current environment', () => {
expect(findEnvironmentsDropdown().attributes().text).toBe(currentEnvironment.name);
});
it('has dropdown items for each environment', () => {
const dropdownItems = findEnvironmentsDropdownItems();
mockEnvironments.forEach((environment, i) => {
const dropdownItem = dropdownItems.at(i);
expect(dropdownItem.text()).toBe(environment.name);
dropdownItem.vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith(
'threatMonitoring/setCurrentEnvironmentId',
environment.id,
);
});
});
it('renders EnvironmentPicker', () => {
expect(findEnvironmentsPicker().exists()).toBe(true);
});
});
......@@ -107,27 +66,33 @@ describe('ThreatMonitoringFilters component', () => {
});
describe.each`
context | isLoadingEnvironments | isLoadingWafStatistics | environments
${'environments are loading'} | ${true} | ${false} | ${mockEnvironments}
${'WAF statistics are loading'} | ${false} | ${true} | ${mockEnvironments}
${'there are no environments'} | ${false} | ${false} | ${[]}
`('given $context', ({ isLoadingEnvironments, isLoadingWafStatistics, environments }) => {
beforeEach(() => {
factory({
environments,
isLoadingEnvironments,
isLoadingWafStatistics,
});
return wrapper.vm.$nextTick();
});
context | isLoadingEnvironments | isLoadingWafStatistics | isLoadingNetworkPolicyStatistics | environments
${'environments are loading'} | ${true} | ${false} | ${false} | ${mockEnvironments}
${'WAF statistics are loading'} | ${false} | ${true} | ${false} | ${mockEnvironments}
${'NetPol statistics are loading'} | ${false} | ${false} | ${true} | ${mockEnvironments}
${'there are no environments'} | ${false} | ${false} | ${false} | ${[]}
`(
'given $context',
({
isLoadingEnvironments,
isLoadingWafStatistics,
isLoadingNetworkPolicyStatistics,
environments,
}) => {
beforeEach(() => {
factory({
environments,
isLoadingEnvironments,
isLoadingWafStatistics,
isLoadingNetworkPolicyStatistics,
});
it('disables the environments dropdown', () => {
expect(findEnvironmentsDropdown().attributes('disabled')).toBe('true');
});
return wrapper.vm.$nextTick();
});
it('disables the "show last" dropdown', () => {
expect(findShowLastDropdown().attributes('disabled')).toBe('true');
});
});
it('disables the "show last" dropdown', () => {
expect(findShowLastDropdown().attributes('disabled')).toBe('true');
});
},
);
});
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