Commit 86667196 authored by ap4y's avatar ap4y

Bootstrap pages and components for policy editor

This commit contains preliminary work for the threat monitoring policy
editor: new route and views, new frontend app, related navigation
changes. Frontend feature flag was also introduced as well as a link
to the new page behind the feature flag.
parent 870cc632
import initPolicyEditorApp from 'ee/threat_monitoring/policy_editor';
document.addEventListener('DOMContentLoaded', initPolicyEditorApp);
...@@ -58,6 +58,10 @@ export default { ...@@ -58,6 +58,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
newPolicyPath: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -193,7 +197,10 @@ export default { ...@@ -193,7 +197,10 @@ export default {
/> />
</gl-tab> </gl-tab>
<gl-tab ref="networkPolicyTab" :title="s__('ThreatMonitoring|Policies')"> <gl-tab ref="networkPolicyTab" :title="s__('ThreatMonitoring|Policies')">
<network-policy-list :documentation-path="documentationPath" /> <network-policy-list
:documentation-path="documentationPath"
:new-policy-path="newPolicyPath"
/>
</gl-tab> </gl-tab>
</gl-tabs> </gl-tabs>
</section> </section>
......
...@@ -16,6 +16,8 @@ import { setUrlFragment } from '~/lib/utils/url_utility'; ...@@ -16,6 +16,8 @@ import { setUrlFragment } from '~/lib/utils/url_utility';
import EnvironmentPicker from './environment_picker.vue'; import EnvironmentPicker from './environment_picker.vue';
import NetworkPolicyEditor from './network_policy_editor.vue'; import NetworkPolicyEditor from './network_policy_editor.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
components: { components: {
GlTable, GlTable,
...@@ -29,11 +31,16 @@ export default { ...@@ -29,11 +31,16 @@ export default {
EnvironmentPicker, EnvironmentPicker,
NetworkPolicyEditor, NetworkPolicyEditor,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
documentationPath: { documentationPath: {
type: String, type: String,
required: true, required: true,
}, },
newPolicyPath: {
type: String,
required: true,
},
}, },
data() { data() {
return { selectedPolicyName: null, initialManifest: null, initialEnforcementStatus: null }; return { selectedPolicyName: null, initialManifest: null, initialEnforcementStatus: null };
...@@ -149,8 +156,17 @@ export default { ...@@ -149,8 +156,17 @@ export default {
</div> </div>
<div class="pt-3 px-3 bg-gray-light"> <div class="pt-3 px-3 bg-gray-light">
<div class="row"> <div class="row justify-content-between align-items-center">
<environment-picker ref="environmentsPicker" /> <environment-picker ref="environmentsPicker" />
<div v-if="glFeatures.networkPolicyEditor" class="col-sm-auto">
<gl-button
category="secondary"
variant="info"
:href="newPolicyPath"
data-testid="new-policy"
>{{ s__('NetworkPolicies|New policy') }}</gl-button
>
</div>
</div> </div>
</div> </div>
......
<script>
export default {
name: 'PolicyEditor',
};
</script>
<template>
<section>
<header class="my-3">
<h2 class="h3 mb-1">
{{ s__('NetworkPolicies|Policy description') }}
</h2>
</header>
</section>
</template>
...@@ -14,6 +14,7 @@ export default () => { ...@@ -14,6 +14,7 @@ export default () => {
emptyStateSvgPath, emptyStateSvgPath,
wafNoDataSvgPath, wafNoDataSvgPath,
networkPolicyNoDataSvgPath, networkPolicyNoDataSvgPath,
newPolicyPath,
documentationPath, documentationPath,
defaultEnvironmentId, defaultEnvironmentId,
showUserCallout, showUserCallout,
...@@ -46,6 +47,7 @@ export default () => { ...@@ -46,6 +47,7 @@ export default () => {
showUserCallout: parseBoolean(showUserCallout), showUserCallout: parseBoolean(showUserCallout),
userCalloutId, userCalloutId,
userCalloutsPath, userCalloutsPath,
newPolicyPath,
}, },
}); });
}, },
......
import Vue from 'vue';
import PolicyEditorApp from './components/policy_editor/app.vue';
import createStore from './store';
export default () => {
const el = document.querySelector('#js-policy-builder-app');
const { networkPoliciesEndpoint } = el.dataset;
const store = createStore();
store.dispatch('networkPolicies/setEndpoints', {
networkPoliciesEndpoint,
});
return new Vue({
el,
store,
render(createElement) {
return createElement(PolicyEditorApp, {});
},
});
};
...@@ -3,5 +3,15 @@ ...@@ -3,5 +3,15 @@
module Projects module Projects
class ThreatMonitoringController < Projects::ApplicationController class ThreatMonitoringController < Projects::ApplicationController
before_action :authorize_read_threat_monitoring! before_action :authorize_read_threat_monitoring!
before_action :verify_network_policy_editor_flag!, only: :new
before_action do
push_frontend_feature_flag(:network_policy_editor, project)
end
private
def verify_network_policy_editor_flag!
render_404 unless Feature.enabled?(:network_policy_editor, project, default_enabled: false)
end
end end
end end
...@@ -133,6 +133,7 @@ module EE ...@@ -133,6 +133,7 @@ module EE
projects/dependencies#index projects/dependencies#index
projects/licenses#index projects/licenses#index
projects/threat_monitoring#show projects/threat_monitoring#show
projects/threat_monitoring#new
] ]
end end
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
%span= _('License Compliance') %span= _('License Compliance')
- if project_nav_tab?(:threat_monitoring) - if project_nav_tab?(:threat_monitoring)
= nav_link(path: 'projects/threat_monitoring#show') do = nav_link(controller: ['projects/threat_monitoring']) do
= link_to project_threat_monitoring_path(@project), title: _('Threat Monitoring') do = link_to project_threat_monitoring_path(@project), title: _('Threat Monitoring') do
%span= _('Threat Monitoring') %span= _('Threat Monitoring')
......
- add_to_breadcrumbs s_("ThreatMonitoring|Threat Monitoring"), project_threat_monitoring_path(@project)
- breadcrumb_title s_("NetworkPolicies|New policy")
- page_title s_("NetworkPolicies|Policy editor")
#js-policy-builder-app{ data: { network_policies_endpoint: project_security_network_policies_path(@project) } }
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
network_policy_statistics_endpoint: summary_project_security_network_policies_path(@project, format: :json), network_policy_statistics_endpoint: summary_project_security_network_policies_path(@project, format: :json),
environments_endpoint: project_environments_path(@project), environments_endpoint: project_environments_path(@project),
network_policies_endpoint: project_security_network_policies_path(@project), network_policies_endpoint: project_security_network_policies_path(@project),
new_policy_path: new_project_threat_monitoring_policy_path(@project),
default_environment_id: default_environment_id, default_environment_id: default_environment_id,
user_callouts_path: user_callouts_path, user_callouts_path: user_callouts_path,
user_callout_id: UserCalloutsHelper::THREAT_MONITORING_INFO, user_callout_id: UserCalloutsHelper::THREAT_MONITORING_INFO,
......
...@@ -37,7 +37,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -37,7 +37,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :subscriptions, only: [:create, :destroy] resources :subscriptions, only: [:create, :destroy]
resource :threat_monitoring, only: [:show], controller: :threat_monitoring resource :threat_monitoring, only: [:show], controller: :threat_monitoring do
resources :policies, only: [:new], controller: :threat_monitoring
end
resources :protected_environments, only: [:create, :update, :destroy], constraints: { id: /\d+/ } do resources :protected_environments, only: [:create, :update, :destroy], constraints: { id: /\d+/ } do
collection do collection do
......
...@@ -6,9 +6,9 @@ RSpec.describe Projects::ThreatMonitoringController do ...@@ -6,9 +6,9 @@ RSpec.describe Projects::ThreatMonitoringController do
let_it_be(:project) { create(:project, :repository, :private) } let_it_be(:project) { create(:project, :repository, :private) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
subject { get :show, params: { namespace_id: project.namespace, project_id: project } }
describe 'GET show' do describe 'GET show' do
subject { get :show, params: { namespace_id: project.namespace, project_id: project } }
context 'with authorized user' do context 'with authorized user' do
before do before do
project.add_developer(user) project.add_developer(user)
...@@ -30,7 +30,87 @@ RSpec.describe Projects::ThreatMonitoringController do ...@@ -30,7 +30,87 @@ RSpec.describe Projects::ThreatMonitoringController do
context 'when feature is not available' do context 'when feature is not available' do
before do before do
stub_feature_flags(threat_monitoring: false) stub_licensed_features(threat_monitoring: false)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'with unauthorized user' do
before do
sign_in(user)
end
context 'when feature is available' do
before do
stub_licensed_features(threat_monitoring: true)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'with anonymous user' do
it 'returns 302' do
subject
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(new_user_session_path)
end
end
end
describe 'GET new' do
subject { get :new, params: { namespace_id: project.namespace, project_id: project } }
context 'with authorized user' do
before do
project.add_developer(user)
sign_in(user)
end
context 'when feature is available' do
before do
stub_licensed_features(threat_monitoring: true)
end
context 'and feature flag is disabled' do
before do
stub_feature_flags(network_policy_editor: false)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'and feature flag is enabled' do
before do
stub_feature_flags(network_policy_editor: true)
end
it 'renders the show template' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:new)
end
end
end
context 'when feature is not available' do
before do
stub_licensed_features(threat_monitoring: false) stub_licensed_features(threat_monitoring: false)
end end
......
...@@ -22,6 +22,7 @@ exports[`ThreatMonitoringApp component given there is a default environment with ...@@ -22,6 +22,7 @@ exports[`ThreatMonitoringApp component given there is a default environment with
> >
<network-policy-list-stub <network-policy-list-stub
documentationpath="/docs" documentationpath="/docs"
newpolicypath="/policy/new"
/> />
</gl-tab-stub> </gl-tab-stub>
`; `;
......
...@@ -6,10 +6,10 @@ exports[`NetworkPolicyList component renders policies table 1`] = ` ...@@ -6,10 +6,10 @@ exports[`NetworkPolicyList component renders policies table 1`] = `
<table <table
aria-busy="false" aria-busy="false"
aria-colcount="3" aria-colcount="3"
aria-describedby="__BVID__45__caption_" aria-describedby="__BVID__143__caption_"
aria-multiselectable="false" aria-multiselectable="false"
class="table b-table gl-table table-hover b-table-stacked-md b-table-selectable b-table-select-single" class="table b-table gl-table table-hover b-table-stacked-md b-table-selectable b-table-select-single"
id="__BVID__45" id="__BVID__143"
role="table" role="table"
> >
<!----> <!---->
......
...@@ -9,6 +9,7 @@ import ThreatMonitoringFilters from 'ee/threat_monitoring/components/threat_moni ...@@ -9,6 +9,7 @@ import ThreatMonitoringFilters from 'ee/threat_monitoring/components/threat_moni
const defaultEnvironmentId = 3; const defaultEnvironmentId = 3;
const documentationPath = '/docs'; const documentationPath = '/docs';
const newPolicyPath = '/policy/new';
const chartEmptyStateSvgPath = '/chart-svgs'; const chartEmptyStateSvgPath = '/chart-svgs';
const emptyStateSvgPath = '/svgs'; const emptyStateSvgPath = '/svgs';
const wafNoDataSvgPath = '/waf-no-data-svg'; const wafNoDataSvgPath = '/waf-no-data-svg';
...@@ -42,6 +43,7 @@ describe('ThreatMonitoringApp component', () => { ...@@ -42,6 +43,7 @@ describe('ThreatMonitoringApp component', () => {
wafNoDataSvgPath, wafNoDataSvgPath,
networkPolicyNoDataSvgPath, networkPolicyNoDataSvgPath,
documentationPath, documentationPath,
newPolicyPath,
showUserCallout: true, showUserCallout: true,
userCalloutId, userCalloutId,
userCalloutsPath, userCalloutsPath,
......
...@@ -13,7 +13,7 @@ describe('NetworkPolicyList component', () => { ...@@ -13,7 +13,7 @@ describe('NetworkPolicyList component', () => {
let store; let store;
let wrapper; let wrapper;
const factory = ({ propsData, state, data } = {}) => { const factory = ({ propsData, state, data, provide } = {}) => {
store = createStore(); store = createStore();
Object.assign(store.state.networkPolicies, { Object.assign(store.state.networkPolicies, {
isLoadingPolicies: false, isLoadingPolicies: false,
...@@ -26,10 +26,12 @@ describe('NetworkPolicyList component', () => { ...@@ -26,10 +26,12 @@ describe('NetworkPolicyList component', () => {
wrapper = mount(NetworkPolicyList, { wrapper = mount(NetworkPolicyList, {
propsData: { propsData: {
documentationPath: 'documentation_path', documentationPath: 'documentation_path',
newPolicyPath: 'new_policy_path',
...propsData, ...propsData,
}, },
data, data,
store, store,
provide,
}); });
}; };
...@@ -56,6 +58,28 @@ describe('NetworkPolicyList component', () => { ...@@ -56,6 +58,28 @@ describe('NetworkPolicyList component', () => {
expect(findEnvironmentsPicker().exists()).toBe(true); expect(findEnvironmentsPicker().exists()).toBe(true);
}); });
it('does not render the new policy button', () => {
const button = wrapper.find('[data-testid="new-policy"]');
expect(button.exists()).toBe(false);
});
describe('given the networkPolicyEditor feature flag is enabled', () => {
beforeEach(() => {
factory({
provide: {
glFeatures: {
networkPolicyEditor: true,
},
},
});
});
it('renders the new policy button', () => {
const button = wrapper.find('[data-testid="new-policy"]');
expect(button.exists()).toBe(true);
});
});
it('renders policies table', () => { it('renders policies table', () => {
expect(findPoliciesTable().element).toMatchSnapshot(); expect(findPoliciesTable().element).toMatchSnapshot();
}); });
......
import { shallowMount } from '@vue/test-utils';
import PolicyEditorApp from 'ee/threat_monitoring/components/policy_editor/app.vue';
import createStore from 'ee/threat_monitoring/store';
describe('PolicyEditorApp component', () => {
let store;
let wrapper;
const factory = ({ propsData, state, options } = {}) => {
store = createStore();
Object.assign(store.state.threatMonitoring, {
...state,
});
wrapper = shallowMount(PolicyEditorApp, {
propsData: {
...propsData,
},
store,
...options,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders the header title', () => {
factory({});
expect(wrapper.find('header').exists()).toBe(true);
});
});
...@@ -154,6 +154,7 @@ RSpec.describe ProjectsHelper do ...@@ -154,6 +154,7 @@ RSpec.describe ProjectsHelper do
projects/dependencies#index projects/dependencies#index
projects/licenses#index projects/licenses#index
projects/threat_monitoring#show projects/threat_monitoring#show
projects/threat_monitoring#new
] ]
end end
......
...@@ -15537,6 +15537,9 @@ msgstr "" ...@@ -15537,6 +15537,9 @@ msgstr ""
msgid "NetworkPolicies|Name" msgid "NetworkPolicies|Name"
msgstr "" msgstr ""
msgid "NetworkPolicies|New policy"
msgstr ""
msgid "NetworkPolicies|No policies detected" msgid "NetworkPolicies|No policies detected"
msgstr "" msgstr ""
...@@ -15549,6 +15552,12 @@ msgstr "" ...@@ -15549,6 +15552,12 @@ msgstr ""
msgid "NetworkPolicies|Policy definition" msgid "NetworkPolicies|Policy definition"
msgstr "" msgstr ""
msgid "NetworkPolicies|Policy description"
msgstr ""
msgid "NetworkPolicies|Policy editor"
msgstr ""
msgid "NetworkPolicies|Something went wrong, failed to update policy" msgid "NetworkPolicies|Something went wrong, failed to update policy"
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