mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
committed by
GitHub
parent
6cadce1598
commit
53ed514803
@@ -1,7 +1,7 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as ruleActions} from '../store/rule';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { actions as policyActions } from '../store/policy';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
@@ -13,98 +13,123 @@ import {
|
||||
RadioChangeEvent,
|
||||
Row,
|
||||
Select,
|
||||
SelectProps,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
Typography
|
||||
} from "antd";
|
||||
import {CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import type {CustomTagProps} from 'rc-select/lib/BaseSelect'
|
||||
import {Rule, RuleToSave} from "../store/rule/types";
|
||||
import {uniq} from "lodash"
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import { CloseOutlined, FlagFilled, QuestionCircleFilled } from "@ant-design/icons";
|
||||
import type { CustomTagProps } from 'rc-select/lib/BaseSelect'
|
||||
import { Policy, PolicyToSave } from "../store/policy/types";
|
||||
import { uniq } from "lodash";
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
const { Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
interface FormRule extends Rule {
|
||||
interface FormPolicy {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
query: string
|
||||
bidirectional: boolean
|
||||
protocol: string
|
||||
ports: string[]
|
||||
action: string
|
||||
tagSourceGroups: string[]
|
||||
tagDestinationGroups: string[]
|
||||
}
|
||||
|
||||
const AccessControlNew = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const { getTokenSilently } = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible)
|
||||
const setupNewPolicyVisible = useSelector((state: RootState) => state.policy.setupNewPolicyVisible)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const rule = useSelector((state: RootState) => state.rule.rule)
|
||||
const savedRule = useSelector((state: RootState) => state.rule.savedRule)
|
||||
const actions: SelectProps['options'] = [
|
||||
{ label: 'Accept', value: 'accept' },
|
||||
{ label: 'Drop', value: 'drop' },
|
||||
]
|
||||
const protocols: SelectProps['options'] = [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
{ label: 'ICMP', value: 'icmp' },
|
||||
]
|
||||
const policy = useSelector((state: RootState) => state.policy.policy)
|
||||
const savedPolicy = useSelector((state: RootState) => state.policy.savedPolicy)
|
||||
|
||||
const [editName, setEditName] = useState(false)
|
||||
const [editDescription, setEditDescription] = useState(false)
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [formRule, setFormRule] = useState({} as FormRule)
|
||||
const [formPolicy, setFormPolicy] = useState({} as FormPolicy)
|
||||
const [form] = Form.useForm()
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const inputDescriptionRef = useRef<any>(null)
|
||||
|
||||
const optionsDisabledEnabled = [{label: 'Enabled', value: false}, {label: 'Disabled', value: true}]
|
||||
const optionsStatusEnabled = [{ label: 'Enabled', value: true }, { label: 'Disabled', value: false }]
|
||||
|
||||
useEffect(() => { if (editName) inputNameRef.current!.focus({ cursor: 'end' }) }, [editName])
|
||||
useEffect(() => { if (editDescription) inputDescriptionRef.current!.focus({ cursor: 'end' }) }, [editDescription])
|
||||
useEffect(() => { setTagGroups(groups?.map(g => g.name) || []) }, [groups])
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editName]);
|
||||
if (!policy) return
|
||||
const fPolicy = {
|
||||
id: policy.id,
|
||||
name: policy.name,
|
||||
description: policy.description,
|
||||
enabled: policy.enabled,
|
||||
query: '',
|
||||
bidirectional: policy.rules[0].bidirectional,
|
||||
protocol: policy.rules[0].protocol,
|
||||
ports: policy.rules[0].ports,
|
||||
action: policy.rules[0].action,
|
||||
tagSourceGroups: policy.rules[0].sources ? policy.rules[0].sources?.map(t => t.name) : [],
|
||||
tagDestinationGroups: policy.rules[0].destinations ? policy.rules[0].destinations?.map(t => t.name) : [],
|
||||
} as FormPolicy
|
||||
setFormPolicy(fPolicy)
|
||||
form.setFieldsValue(fPolicy)
|
||||
}, [policy, form])
|
||||
|
||||
useEffect(() => {
|
||||
if (editDescription) inputDescriptionRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rule) return
|
||||
const fRule = {
|
||||
...rule,
|
||||
tagSourceGroups: rule.sources ? rule.sources?.map(t => t.name) : [],
|
||||
tagDestinationGroups: rule.destinations ? rule.destinations?.map(t => t.name) : []
|
||||
} as FormRule
|
||||
setFormRule(fRule)
|
||||
form.setFieldsValue(fRule)
|
||||
}, [rule])
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.map(g => g.name) || [])
|
||||
}, [groups])
|
||||
|
||||
const createRuleToSave = (): RuleToSave => {
|
||||
const sources = groups?.filter(g => formRule.tagSourceGroups.includes(g.name)).map(g => g.id || '') || []
|
||||
const destinations = groups?.filter(g => formRule.tagDestinationGroups.includes(g.name)).map(g => g.id || '') || []
|
||||
const sourcesNoId = formRule.tagSourceGroups.filter(s => !tagGroups.includes(s))
|
||||
const destinationsNoId = formRule.tagDestinationGroups.filter(s => !tagGroups.includes(s))
|
||||
const createPolicyToSave = (): PolicyToSave => {
|
||||
const sources = groups?.filter(g => formPolicy.tagSourceGroups.includes(g.name)).map(g => g.id || '') || []
|
||||
const destinations = groups?.filter(g => formPolicy.tagDestinationGroups.includes(g.name)).map(g => g.id || '') || []
|
||||
const sourcesNoId = formPolicy.tagSourceGroups.filter(s => !tagGroups.includes(s))
|
||||
const destinationsNoId = formPolicy.tagDestinationGroups.filter(s => !tagGroups.includes(s))
|
||||
const groupsToSave = uniq([...sourcesNoId, ...destinationsNoId])
|
||||
return {
|
||||
id: formRule.id,
|
||||
name: formRule.name,
|
||||
description: formRule.description,
|
||||
sources,
|
||||
destinations,
|
||||
id: formPolicy.id,
|
||||
name: formPolicy.name,
|
||||
description: formPolicy.description,
|
||||
enabled: formPolicy.enabled,
|
||||
sourcesNoId,
|
||||
destinationsNoId,
|
||||
groupsToSave,
|
||||
flow: formRule.flow,
|
||||
disabled: formRule.disabled
|
||||
} as RuleToSave
|
||||
rules: [{
|
||||
id: formPolicy.id,
|
||||
name: formPolicy.name,
|
||||
description: formPolicy.description,
|
||||
enabled: formPolicy.enabled,
|
||||
sources,
|
||||
destinations,
|
||||
bidirectional: formPolicy.bidirectional,
|
||||
protocol: formPolicy.protocol,
|
||||
ports: formPolicy.ports,
|
||||
action: 'accept',
|
||||
}],
|
||||
} as PolicyToSave
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
const ruleToSave = createRuleToSave()
|
||||
dispatch(ruleActions.saveRule.request({
|
||||
.then((_) => {
|
||||
const policyToSave = createPolicyToSave()
|
||||
dispatch(policyActions.savePolicy.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: ruleToSave
|
||||
payload: policyToSave
|
||||
}))
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
@@ -113,50 +138,82 @@ const AccessControlNew = () => {
|
||||
};
|
||||
|
||||
const setVisibleNewRule = (status: boolean) => {
|
||||
dispatch(ruleActions.setSetupNewRuleVisible(status));
|
||||
dispatch(policyActions.setSetupNewPolicyVisible(status));
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedRule.loading) return
|
||||
if (savedPolicy.loading) return
|
||||
setEditName(false)
|
||||
dispatch(ruleActions.setRule({
|
||||
dispatch(policyActions.setPolicy({
|
||||
name: '',
|
||||
description: '',
|
||||
sources: [],
|
||||
destinations: [],
|
||||
flow: 'bidirect',
|
||||
disabled: false
|
||||
} as Rule))
|
||||
enabled: true,
|
||||
query: '',
|
||||
rules: [{
|
||||
name: '',
|
||||
description: '',
|
||||
enabled: true,
|
||||
sources: [],
|
||||
destinations: [],
|
||||
bidirectional: true,
|
||||
protocol: 'all',
|
||||
ports: [],
|
||||
action: 'accept',
|
||||
}],
|
||||
} as Policy))
|
||||
setVisibleNewRule(false)
|
||||
}
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormRule({...formRule, ...data})
|
||||
setFormPolicy({ ...formPolicy, ...data })
|
||||
}
|
||||
|
||||
const handleChangeSource = (value: string[]) => {
|
||||
setFormRule({
|
||||
...formRule,
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
tagSourceGroups: value
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const handleChangeDestination = (value: string[]) => {
|
||||
setFormRule({
|
||||
...formRule,
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
tagDestinationGroups: value
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const handleChangeDisabled = ({target: {value}}: RadioChangeEvent) => {
|
||||
setFormRule({
|
||||
...formRule,
|
||||
disabled: value
|
||||
const handleChangeProtocol = (value: string) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
bidirectional: (value === 'all' || value === 'icmp') ? true : formPolicy.bidirectional,
|
||||
ports: (value === 'all' || value === 'icmp') ? [] : formPolicy.ports,
|
||||
protocol: value
|
||||
})
|
||||
}
|
||||
|
||||
const handleChangePorts = (value: string[]) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
ports: value
|
||||
})
|
||||
}
|
||||
|
||||
const handleChangeDisabled = ({ target: { value } }: RadioChangeEvent) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
enabled: value
|
||||
})
|
||||
}
|
||||
|
||||
const handleChangeBidirect = (checked: boolean) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
bidirectional: checked,
|
||||
})
|
||||
};
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const {label, value, closable, onClose} = props;
|
||||
const { value, closable, onClose } = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -168,7 +225,7 @@ const AccessControlNew = () => {
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginRight: 3}}
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
@@ -183,28 +240,47 @@ const AccessControlNew = () => {
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
<span style={{ fontSize: ".85em" }}>{peersCount}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
const dropDownRenderGroups = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{margin: '8px 0'}}/>
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<Row style={{ padding: '0 8px 4px' }}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
<span style={{ color: "#9CA3AF" }}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"/>
|
||||
fill="#9CA3AF" />
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
const dropDownRenderPorts = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<Row style={{ padding: '0 8px 4px' }}>
|
||||
<Col flex="auto">
|
||||
<span style={{ color: "#9CA3AF" }}>Add new ports or range by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF" />
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -231,7 +307,7 @@ const AccessControlNew = () => {
|
||||
return Promise.reject(new Error("Please enter at least one group"))
|
||||
}
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
value.forEach(function(v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
@@ -244,47 +320,69 @@ const AccessControlNew = () => {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const selectPortRangeValidator = (_: RuleObject, value: string[]) => {
|
||||
if (value) {
|
||||
var failed = false
|
||||
value.forEach(function(v: string) {
|
||||
let p = Number(v)
|
||||
if (Number.isNaN(p) || p < 1 || p > 65535) {
|
||||
failed = true
|
||||
return
|
||||
}
|
||||
})
|
||||
if (failed) {
|
||||
return Promise.reject(new Error("Port value must be in 1..65535 range"))
|
||||
}
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const selectPortProtocolValidator = (_: RuleObject, value: string[]) => {
|
||||
if (!formPolicy.bidirectional && value.length === 0) {
|
||||
return Promise.reject(new Error("Directional traffic require ports"))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{rule &&
|
||||
{policy &&
|
||||
<Drawer
|
||||
//title={`${formRule.ID ? 'Edit Rule' : 'New Rule'}`}
|
||||
headerStyle={{display: "none"}}
|
||||
headerStyle={{ display: "none" }}
|
||||
forceRender={true}
|
||||
// width={512}
|
||||
visible={setupNewRuleVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
visible={setupNewPolicyVisible}
|
||||
bodyStyle={{ paddingBottom: 80 }}
|
||||
onClose={onCancel}
|
||||
autoFocus={true}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedRule.loading}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedRule.loading}
|
||||
onClick={handleFormSubmit}>{`${formRule.id ? 'Save' : 'Create'}`}</Button>
|
||||
<Space style={{ display: 'flex', justifyContent: 'end' }}>
|
||||
<Button onClick={onCancel} disabled={savedPolicy.loading}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedPolicy.loading}
|
||||
onClick={handleFormSubmit}>{`${formPolicy.id ? 'Save' : 'Create'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
<Header style={{ margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px" }}>
|
||||
<Row align="top">
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && !editDescription && formRule.id &&
|
||||
<Col flex="none" style={{ display: "flex" }}>
|
||||
{!editName && !editDescription && formPolicy.id &&
|
||||
<button type="button" aria-label="Close" className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
style={{ paddingTop: 3 }}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close"
|
||||
className="anticon anticon-close">
|
||||
<CloseOutlined size={16}/>
|
||||
className="anticon anticon-close">
|
||||
<CloseOutlined size={16} />
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
{!editName && formRule.id ? (
|
||||
{!editName && formPolicy.id ? (
|
||||
<div className={"access-control input-text ant-drawer-title"}
|
||||
onClick={() => toggleEditName(true)}>{formRule.id ? formRule.name : 'New Rule'}</div>
|
||||
onClick={() => toggleEditName(true)}>{formPolicy.id ? formPolicy.name : 'New Rule'}</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
@@ -296,25 +394,25 @@ const AccessControlNew = () => {
|
||||
}]}
|
||||
>
|
||||
<Input placeholder="Add rule name..." ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)} autoComplete="off"/>
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)} autoComplete="off" />
|
||||
</Form.Item>
|
||||
)}
|
||||
{!editDescription ? (
|
||||
<div className={"access-control input-text ant-drawer-subtitle"}
|
||||
onClick={() => toggleEditDescription(true)}>
|
||||
{formRule.description && formRule.description.trim() !== "" ? formRule.description : 'Add description...'}
|
||||
onClick={() => toggleEditDescription(true)}>
|
||||
{formPolicy.description && formPolicy.description.trim() !== "" ? formPolicy.description : 'Add description...'}
|
||||
</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{marginTop: 24}}
|
||||
style={{ marginTop: 24 }}
|
||||
>
|
||||
<Input placeholder="Add description..." ref={inputDescriptionRef}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off"/>
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off" />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
@@ -332,15 +430,16 @@ const AccessControlNew = () => {
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="disabled"
|
||||
name="enabled"
|
||||
label="Status"
|
||||
>
|
||||
|
||||
<Radio.Group
|
||||
options={optionsDisabledEnabled}
|
||||
options={optionsStatusEnabled}
|
||||
onChange={handleChangeDisabled}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
defaultValue={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -348,14 +447,14 @@ const AccessControlNew = () => {
|
||||
<Form.Item
|
||||
name="tagSourceGroups"
|
||||
label="Source groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
rules={[{ validator: selectValidator }]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Tags Mode"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeSource}
|
||||
dropdownRender={dropDownRender}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Tags Mode"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeSource}
|
||||
dropdownRender={dropDownRenderGroups}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
@@ -365,18 +464,32 @@ const AccessControlNew = () => {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="bidirectional"
|
||||
label="Bi-Direct traffic flow"
|
||||
tooltip="Protocol type 'All' or 'ICMP' must be bi-directional. Directional traffic for TCP and UDP protocol requires at least one port to be defined."
|
||||
>
|
||||
<Switch
|
||||
size={"small"}
|
||||
disabled={formPolicy.protocol === "all" || formPolicy.protocol === "icmp"}
|
||||
checked={formPolicy.bidirectional}
|
||||
onChange={handleChangeBidirect}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="tagDestinationGroups"
|
||||
label="Destination groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
rules={[{ validator: selectValidator }]}
|
||||
>
|
||||
<Select
|
||||
mode="tags" style={{width: '100%'}}
|
||||
mode="tags" style={{ width: '100%' }}
|
||||
placeholder="Tags Mode"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeDestination}
|
||||
dropdownRender={dropDownRender}
|
||||
dropdownRender={dropDownRenderGroups}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
@@ -386,22 +499,71 @@ const AccessControlNew = () => {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="protocol"
|
||||
label="Protocol"
|
||||
>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
options={protocols}
|
||||
onChange={handleChangeProtocol}
|
||||
defaultValue={'all'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="ports"
|
||||
label="Ports"
|
||||
rules={[
|
||||
{ message: "Directional traffic requires at least one port",
|
||||
validator: selectPortProtocolValidator, required: false },
|
||||
{ message: "Port value must be in 1..65535 range",
|
||||
validator: selectPortRangeValidator, required: false },
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
mode="tags" style={{ width: '100%' }}
|
||||
placeholder="Tags Mode"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangePorts}
|
||||
dropdownRender={dropDownRenderPorts}
|
||||
disabled={formPolicy.protocol === "all" || formPolicy.protocol === "icmp"}
|
||||
>
|
||||
{
|
||||
formPolicy &&
|
||||
formPolicy.ports?.map(m =>
|
||||
<Option key={m}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
<strong>{m}</strong>
|
||||
</Tag>
|
||||
</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Row wrap={false} gutter={12}>
|
||||
<Col flex="none">
|
||||
<FlagFilled/>
|
||||
<FlagFilled />
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<Paragraph>
|
||||
At the moment access rules are bi-directional by default, this means both
|
||||
source and destination can talk to each-other in both directions. However
|
||||
destination peers will not be able to communicate with each other, nor will
|
||||
the source peers.
|
||||
The default behavior is to drop all traffic that doesn't match an Access control rule.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
If you want to enable all peers of the same group to talk to each other -
|
||||
you can add that group both as a receiver and as a destination.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Protocol type <strong>All</strong> or <strong>ICMP</strong> must be bi-directional.
|
||||
Directional traffic for <strong>TCP</strong> and <strong>UDP</strong> protocol requires at least one port to be defined.
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
@@ -419,4 +581,4 @@ const AccessControlNew = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default AccessControlNew
|
||||
export default AccessControlNew
|
||||
|
||||
@@ -5,6 +5,7 @@ import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
import { sagas as peerSagas } from './peer';
|
||||
import { sagas as setupKeySagas } from './setup-key';
|
||||
import { sagas as userSagas } from './user';
|
||||
import { sagas as policySagas } from './policy';
|
||||
import { sagas as ruleSagas } from './rule';
|
||||
import { sagas as groupSagas } from './group';
|
||||
import { sagas as routeSagas } from './route';
|
||||
@@ -28,6 +29,7 @@ sagaMiddleware.run(peerSagas);
|
||||
sagaMiddleware.run(setupKeySagas);
|
||||
sagaMiddleware.run(userSagas);
|
||||
sagaMiddleware.run(ruleSagas);
|
||||
sagaMiddleware.run(policySagas);
|
||||
sagaMiddleware.run(groupSagas);
|
||||
sagaMiddleware.run(routeSagas);
|
||||
sagaMiddleware.run(nameserverGroupSagas);
|
||||
@@ -36,4 +38,4 @@ sagaMiddleware.run(dnsSettingsSagas);
|
||||
sagaMiddleware.run(accountSagas);
|
||||
sagaMiddleware.run(personalAccessTokenSagas);
|
||||
|
||||
export { apiClient, rootReducer, store };
|
||||
export { apiClient, rootReducer, store };
|
||||
|
||||
34
src/store/policy/actions.ts
Normal file
34
src/store/policy/actions.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
|
||||
import { Policy, PolicyToSave } from './types';
|
||||
import { ApiError, CreateResponse, DeleteResponse, RequestPayload } from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getPolicies: createAsyncAction(
|
||||
'GET_POLICIES_REQUEST',
|
||||
'GET_POLICIES_SUCCESS',
|
||||
'GET_POLICIES_FAILURE',
|
||||
)<RequestPayload<null>, Policy[], ApiError>(),
|
||||
|
||||
savePolicy: createAsyncAction(
|
||||
'SAVE_POLICY_REQUEST',
|
||||
'SAVE_POLICY_SUCCESS',
|
||||
'SAVE_POLICY_FAILURE',
|
||||
)<RequestPayload<PolicyToSave>, CreateResponse<Policy | null>, CreateResponse<Policy | null>>(),
|
||||
setSavedPolicy: createAction('SET_CREATE_POLICY')<CreateResponse<Policy | null>>(),
|
||||
resetSavedPolicy: createAction('RESET_CREATE_POLICY')<null>(),
|
||||
|
||||
deletePolicy: createAsyncAction(
|
||||
'DELETE_POLICY_REQUEST',
|
||||
'DELETE_POLICY_SUCCESS',
|
||||
'DELETE_POLICY_FAILURE'
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
setDeletedPolicy: createAction('SET_DELETED_POLICY')<DeleteResponse<string | null>>(),
|
||||
resetDeletedPolicy: createAction('RESET_DELETED_POLICY')<null>(),
|
||||
removePolicy: createAction('REMOVE_POLICY')<string>(),
|
||||
|
||||
setPolicy: createAction('SET_POLICY')<Policy>(),
|
||||
setSetupNewPolicyVisible: createAction('SET_SETUP_NEW_POLICY_VISIBLE')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
export default actions;
|
||||
7
src/store/policy/index.ts
Normal file
7
src/store/policy/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import actions, { ActionTypes as _actionTypes } from './actions';
|
||||
import reducer from './reducer';
|
||||
import sagas from './sagas';
|
||||
|
||||
export type ActionTypes = _actionTypes;
|
||||
|
||||
export { actions, reducer, sagas };
|
||||
89
src/store/policy/reducer.ts
Normal file
89
src/store/policy/reducer.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { Policy } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import { ApiError, DeleteResponse, CreateResponse } from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: Policy[] | null;
|
||||
policy: Policy | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
deletePolicy: DeleteResponse<string | null>;
|
||||
savedPolicy: CreateResponse<Policy | null>;
|
||||
setupNewPolicyVisible: boolean
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
policy: null,
|
||||
loading: false,
|
||||
failed: null,
|
||||
saving: false,
|
||||
deletePolicy: <DeleteResponse<string | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
},
|
||||
savedPolicy: <CreateResponse<Policy | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
},
|
||||
setupNewPolicyVisible: false
|
||||
};
|
||||
|
||||
const data = createReducer<Policy[], ActionTypes>(initialState.data as Policy[])
|
||||
.handleAction(actions.getPolicies.success, (_, action) => action.payload)
|
||||
.handleAction(actions.getPolicies.failure, () => []);
|
||||
|
||||
const policy = createReducer<Policy, ActionTypes>(initialState.policy as Policy)
|
||||
.handleAction(actions.setPolicy, (store, action) => action.payload);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getPolicies.request, () => true)
|
||||
.handleAction(actions.getPolicies.success, () => false)
|
||||
.handleAction(actions.getPolicies.failure, () => false);
|
||||
|
||||
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getPolicies.request, () => null)
|
||||
.handleAction(actions.getPolicies.success, () => null)
|
||||
.handleAction(actions.getPolicies.failure, (store, action) => action.payload);
|
||||
|
||||
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
|
||||
.handleAction(actions.getPolicies.request, () => true)
|
||||
.handleAction(actions.getPolicies.success, () => false)
|
||||
.handleAction(actions.getPolicies.failure, () => false);
|
||||
|
||||
const deletedPolicy = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletePolicy)
|
||||
.handleAction(actions.deletePolicy.request, () => initialState.deletePolicy)
|
||||
.handleAction(actions.deletePolicy.success, (store, action) => action.payload)
|
||||
.handleAction(actions.deletePolicy.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setDeletedPolicy, (store, action) => action.payload)
|
||||
.handleAction(actions.resetDeletedPolicy, () => initialState.deletePolicy)
|
||||
|
||||
const savedPolicy = createReducer<CreateResponse<Policy | null>, ActionTypes>(initialState.savedPolicy)
|
||||
.handleAction(actions.savePolicy.request, () => initialState.savedPolicy)
|
||||
.handleAction(actions.savePolicy.success, (store, action) => action.payload)
|
||||
.handleAction(actions.savePolicy.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedPolicy, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedPolicy, () => initialState.savedPolicy)
|
||||
|
||||
const setupNewPolicyVisible = createReducer<boolean, ActionTypes>(initialState.setupNewPolicyVisible)
|
||||
.handleAction(actions.setSetupNewPolicyVisible, (store, action) => action.payload)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
policy,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedPolicy,
|
||||
savedPolicy,
|
||||
setupNewPolicyVisible
|
||||
});
|
||||
167
src/store/policy/sagas.ts
Normal file
167
src/store/policy/sagas.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { all, call, put, select, takeLatest } from 'redux-saga/effects';
|
||||
import { ApiError, ApiResponse, CreateResponse, DeleteResponse } from '../../services/api-client/types';
|
||||
import { Policy, PolicyRule } from './types'
|
||||
import service from './service';
|
||||
import serviceGroup from '../group/service';
|
||||
import actions from './actions';
|
||||
import { actions as groupActions } from '../group';
|
||||
import { Group } from "../group/types";
|
||||
|
||||
export function* getPolicies(action: ReturnType<typeof actions.getPolicies.request>): Generator {
|
||||
try {
|
||||
|
||||
yield put(actions.setDeletedPolicy({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>))
|
||||
|
||||
const effect = yield call(service.getPolicies, action.payload);
|
||||
const response = effect as ApiResponse<Policy[]>;
|
||||
|
||||
yield put(actions.getPolicies.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getPolicies.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setCreatedPolicy(action: ReturnType<typeof actions.setSavedPolicy>): Generator {
|
||||
yield put(actions.setSavedPolicy(action.payload))
|
||||
}
|
||||
|
||||
function getNewGroupIds(dataString: string[], responses: Group[]): string[] {
|
||||
return responses.filter(r => dataString.includes(r.name)).map(r => r.id || '')
|
||||
}
|
||||
|
||||
export function* savePolicy(action: ReturnType<typeof actions.savePolicy.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setSavedPolicy({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as CreateResponse<Policy | null>))
|
||||
|
||||
const policyToSave = action.payload.payload
|
||||
|
||||
const responsesGroup = yield all(policyToSave.groupsToSave.map(g => call(serviceGroup.createGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: { name: g }
|
||||
})
|
||||
))
|
||||
|
||||
const resGroups = (responsesGroup as ApiResponse<Policy>[]).filter(r => r.statusCode === 200).map(r => (r.body as Group))
|
||||
|
||||
const currentGroups = [...(yield select(state => state.group.data)) as Policy[]]
|
||||
const newGroups = [...currentGroups, ...resGroups]
|
||||
yield put(groupActions.getGroups.success(newGroups));
|
||||
|
||||
const newSources = getNewGroupIds(policyToSave.sourcesNoId, resGroups)
|
||||
const newDestinations = getNewGroupIds(policyToSave.destinationsNoId, resGroups)
|
||||
|
||||
const payloadToSave = {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {
|
||||
name: policyToSave.name,
|
||||
description: policyToSave.description,
|
||||
enabled: policyToSave.enabled,
|
||||
query: policyToSave.query
|
||||
} as Policy
|
||||
}
|
||||
if (policyToSave.rules.length > 0) {
|
||||
payloadToSave.payload.rules = []
|
||||
}
|
||||
policyToSave.rules.forEach((r) => {
|
||||
payloadToSave.payload.rules.push({
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
enabled: r.enabled,
|
||||
sources: [...r.sources as string[], ...newSources],
|
||||
destinations: [...r.destinations as string[], ...newDestinations],
|
||||
bidirectional: r.bidirectional,
|
||||
protocol: r.protocol,
|
||||
ports: r.ports,
|
||||
action: r.action
|
||||
} as PolicyRule)
|
||||
})
|
||||
|
||||
let effect
|
||||
if (!policyToSave.id) {
|
||||
effect = yield call(service.createPolicy, payloadToSave);
|
||||
} else {
|
||||
payloadToSave.payload.id = policyToSave.id
|
||||
effect = yield call(service.editPolicy, payloadToSave);
|
||||
}
|
||||
|
||||
const response = effect as ApiResponse<Policy>;
|
||||
|
||||
yield put(actions.savePolicy.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as CreateResponse<Policy | null>));
|
||||
|
||||
yield put(groupActions.getGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
yield put(actions.getPolicies.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
} catch (err) {
|
||||
yield put(actions.savePolicy.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as CreateResponse<Policy | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setDeletePolicy(action: ReturnType<typeof actions.setDeletedPolicy>): Generator {
|
||||
yield put(actions.setDeletedPolicy(action.payload))
|
||||
}
|
||||
|
||||
export function* deletePolicy(action: ReturnType<typeof actions.deletePolicy.request>): Generator {
|
||||
try {
|
||||
yield call(actions.setDeletedPolicy, {
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>)
|
||||
|
||||
const effect = yield call(service.deletedPolicy, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
yield put(actions.deletePolicy.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
const policies = (yield select(state => state.policy.data)) as Policy[]
|
||||
yield put(actions.getPolicies.success(policies.filter((p: Policy) => p.id !== action.payload.payload)))
|
||||
} catch (err) {
|
||||
yield put(actions.deletePolicy.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getPolicies.request, getPolicies),
|
||||
takeLatest(actions.savePolicy.request, savePolicy),
|
||||
takeLatest(actions.deletePolicy.request, deletePolicy)
|
||||
]);
|
||||
}
|
||||
|
||||
32
src/store/policy/service.ts
Normal file
32
src/store/policy/service.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ApiResponse, RequestPayload } from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { Policy } from './types';
|
||||
|
||||
export default {
|
||||
async getPolicies(payload: RequestPayload<null>): Promise<ApiResponse<Policy[]>> {
|
||||
return apiClient.get<Policy[]>(
|
||||
`/api/policies`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletedPolicy(payload: RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/policies/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createPolicy(payload: RequestPayload<Policy>): Promise<ApiResponse<Policy>> {
|
||||
return apiClient.post<Policy>(
|
||||
`/api/policies`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editPolicy(payload: RequestPayload<Policy>): Promise<ApiResponse<Policy>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<Policy>(
|
||||
`/api/policies/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
29
src/store/policy/types.ts
Normal file
29
src/store/policy/types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Group } from "../group/types";
|
||||
|
||||
export interface PolicyRule {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
sources: Group[] | string[] | null
|
||||
destinations: Group[] | string[] | null
|
||||
bidirectional: boolean
|
||||
action: string
|
||||
protocol: string
|
||||
ports: string[]
|
||||
}
|
||||
|
||||
export interface Policy {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
query: string
|
||||
rules: PolicyRule[]
|
||||
};
|
||||
|
||||
export interface PolicyToSave extends Policy {
|
||||
sourcesNoId: string[],
|
||||
destinationsNoId: string[],
|
||||
groupsToSave: string[]
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { reducer as setupKey } from './setup-key';
|
||||
import { reducer as user } from './user';
|
||||
import { reducer as group } from './group';
|
||||
import { reducer as rule } from './rule';
|
||||
import { reducer as policy } from './policy';
|
||||
import { reducer as route } from './route';
|
||||
import { reducer as nameserverGroup } from './nameservers';
|
||||
import { reducer as event } from './event';
|
||||
@@ -13,15 +14,16 @@ import { reducer as account } from './account';
|
||||
import { reducer as personalAccessToken } from './personal-access-token';
|
||||
|
||||
export default combineReducers({
|
||||
peer,
|
||||
setupKey,
|
||||
user,
|
||||
group,
|
||||
rule,
|
||||
route,
|
||||
nameserverGroup,
|
||||
event,
|
||||
dnsSettings,
|
||||
account,
|
||||
personalAccessToken
|
||||
peer,
|
||||
setupKey,
|
||||
user,
|
||||
group,
|
||||
rule,
|
||||
policy,
|
||||
route,
|
||||
nameserverGroup,
|
||||
event,
|
||||
dnsSettings,
|
||||
account,
|
||||
personalAccessToken
|
||||
});
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { ApiResponse, RequestPayload } from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { Rule } from './types';
|
||||
import {SetupKey} from "../setup-key/types";
|
||||
|
||||
export default {
|
||||
async getRules(payload:RequestPayload<null>): Promise<ApiResponse<Rule[]>> {
|
||||
return apiClient.get<Rule[]>(
|
||||
`/api/rules`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletedRule(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/rules/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
|
||||
return apiClient.post<Rule>(
|
||||
`/api/rules`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<Rule>(
|
||||
`/api/rules/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async getRules(payload: RequestPayload<null>): Promise<ApiResponse<Rule[]>> {
|
||||
return apiClient.get<Rule[]>(
|
||||
`/api/rules`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletedRule(payload: RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/rules/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createRule(payload: RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
|
||||
return apiClient.post<Rule>(
|
||||
`/api/rules`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editRule(payload: RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<Rule>(
|
||||
`/api/rules/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Group} from "../group/types";
|
||||
import { Group } from "../group/types";
|
||||
|
||||
export interface Rule {
|
||||
id?: string
|
||||
@@ -7,6 +7,8 @@ export interface Rule {
|
||||
sources: Group[] | string[] | null
|
||||
destinations: Group[] | string[] | null
|
||||
flow: string
|
||||
protocol: string
|
||||
ports: string[]
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
@@ -14,4 +16,4 @@ export interface RuleToSave extends Rule {
|
||||
sourcesNoId: string[],
|
||||
destinationsNoId: string[],
|
||||
groupsToSave: string[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -23,68 +23,76 @@ import {
|
||||
import {Container} from "../components/Container";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {Rule} from "../store/rule/types";
|
||||
import {actions as ruleActions} from "../store/rule";
|
||||
import {Policy} from "../store/policy/types";
|
||||
import {actions as policyActions} from "../store/policy";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {filter, sortBy} from "lodash";
|
||||
import {CloseOutlined, EllipsisOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import {EllipsisOutlined, ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import bidirect from '../assets/direct_bi.svg';
|
||||
import inbound from '../assets/direct_in.svg';
|
||||
import outbound from '../assets/direct_out.svg';
|
||||
import AccessControlNew from "../components/AccessControlNew";
|
||||
import {Group} from "../store/group/types";
|
||||
import { Group } from "../store/group/types";
|
||||
import AccessControlModalGroups from "../components/AccessControlModalGroups";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
import {PeerDataTable} from "../store/peer/types";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
const {confirm} = Modal;
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
const { Column } = Table;
|
||||
const { confirm } = Modal;
|
||||
|
||||
interface RuleDataTable extends Rule {
|
||||
interface PolicyDataTable {
|
||||
id?: string
|
||||
key: string;
|
||||
sourceCount: number;
|
||||
sourceLabel: '';
|
||||
destinationCount: number;
|
||||
destinationLabel: '';
|
||||
name: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
query: string
|
||||
sources: string[]
|
||||
destinations: string[]
|
||||
bidirectional: boolean
|
||||
protocol: string
|
||||
ports: string[]
|
||||
sourceCount: number
|
||||
sourceLabel: ''
|
||||
destinationCount: number
|
||||
destinationLabel: ''
|
||||
}
|
||||
|
||||
interface GroupsToShow {
|
||||
title: string,
|
||||
groups: Group[] | string[] | null,
|
||||
title: string
|
||||
groups: Group[] | string[] | null
|
||||
modalVisible: boolean
|
||||
}
|
||||
|
||||
export const AccessControl = () => {
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const { onChangePageSize, pageSizeOptions, pageSize } = usePageSizeHelpers()
|
||||
const { getTokenSilently } = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const rules = useSelector((state: RootState) => state.rule.data);
|
||||
const failed = useSelector((state: RootState) => state.rule.failed);
|
||||
const loading = useSelector((state: RootState) => state.rule.loading);
|
||||
const deletedRule = useSelector((state: RootState) => state.rule.deletedRule);
|
||||
const savedRule = useSelector((state: RootState) => state.rule.savedRule);
|
||||
const policies = useSelector((state: RootState) => state.policy.data);
|
||||
const failed = useSelector((state: RootState) => state.policy.failed);
|
||||
const loading = useSelector((state: RootState) => state.policy.loading);
|
||||
const deletedPolicy = useSelector((state: RootState) => state.policy.deletedPolicy);
|
||||
const savedPolicy = useSelector((state: RootState) => state.policy.savedPolicy);
|
||||
|
||||
const [showTutorial, setShowTutorial] = useState(true)
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionAllEnable, setOptionAllEnable] = useState('enabled');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [dataTable, setDataTable] = useState([] as RuleDataTable[]);
|
||||
const [ruleToAction, setRuleToAction] = useState(null as RuleDataTable | null);
|
||||
const [dataTable, setDataTable] = useState([] as PolicyDataTable[]);
|
||||
const [policyToAction, setPolicyToAction] = useState(null as PolicyDataTable | null);
|
||||
const [groupsToShow, setGroupsToShow] = useState({} as GroupsToShow)
|
||||
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible);
|
||||
const setupNewPolicyVisible = useSelector((state: RootState) => state.policy.setupNewPolicyVisible);
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState("")
|
||||
|
||||
|
||||
const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'}, {label: 'All', value: 'all'}]
|
||||
const optionsAllEnabled = [{ label: 'Enabled', value: 'enabled' }, { label: 'All', value: 'all' }]
|
||||
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
key: "view",
|
||||
label: (<Button type="text" block onClick={() => onClickViewRule()}>View</Button>)
|
||||
label: (<Button type="text" block onClick={() => onClickViewPolicy()}>View</Button>)
|
||||
},
|
||||
// {
|
||||
// key: "delete",
|
||||
@@ -101,88 +109,97 @@ export const AccessControl = () => {
|
||||
return (!data) ? "No group" : (data.length > 1) ? `${data.length} Groups` : (data.length === 1) ? data[0].name : "No group"
|
||||
}
|
||||
|
||||
const isShowTutorial = (rules: Rule[]): boolean => {
|
||||
return (!rules.length || (rules.length === 1 && rules[0].name === "Default"))
|
||||
const isShowTutorial = (policy: Policy[]): boolean => {
|
||||
return (!policy.length || (policy.length === 1 && policy[0].name === "Default"))
|
||||
}
|
||||
|
||||
const transformDataTable = (d: Rule[]): RuleDataTable[] => {
|
||||
return d.map(p => {
|
||||
const sourceLabel = getSourceDestinationLabel(p.sources as Group[])
|
||||
const destinationLabel = getSourceDestinationLabel(p.destinations as Group[])
|
||||
const transformDataTable = (d: Policy[]): PolicyDataTable[] => {
|
||||
return d.map(policy => {
|
||||
const sourceLabel = getSourceDestinationLabel(policy.rules[0].sources as Group[])
|
||||
const destinationLabel = getSourceDestinationLabel(policy.rules[0].destinations as Group[])
|
||||
return {
|
||||
key: p.id, ...p,
|
||||
sourceCount: p.sources?.length,
|
||||
id: policy.id,
|
||||
key: policy.id,
|
||||
name: policy.name,
|
||||
description: policy.description,
|
||||
enabled: policy.enabled,
|
||||
sources: policy.rules[0].sources,
|
||||
destinations: policy.rules[0].destinations,
|
||||
bidirectional: policy.rules[0].bidirectional,
|
||||
sourceCount: policy.rules[0].sources?.length,
|
||||
sourceLabel,
|
||||
destinationCount: p.destinations?.length,
|
||||
destinationLabel
|
||||
} as RuleDataTable
|
||||
destinationCount: policy.rules[0].destinations?.length,
|
||||
destinationLabel,
|
||||
protocol: policy.rules[0].protocol,
|
||||
ports: policy.rules[0].ports,
|
||||
} as PolicyDataTable
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(ruleActions.getRules.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(policyActions.getPolicies.request({ getAccessTokenSilently: getTokenSilently, payload: null }));
|
||||
dispatch(groupActions.getGroups.request({ getAccessTokenSilently: getTokenSilently, payload: null }));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (failed) {
|
||||
setShowTutorial(false)
|
||||
} else {
|
||||
setShowTutorial(isShowTutorial(rules))
|
||||
setShowTutorial(isShowTutorial(policies))
|
||||
setDataTable(sortBy(transformDataTable(filterDataTable()), "name"))
|
||||
}
|
||||
}, [rules])
|
||||
}, [policies])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch, optionAllEnable])
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
const styleNotification = { marginTop: 85 }
|
||||
|
||||
const saveKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedRule.loading) {
|
||||
message.loading({content: 'Saving...', key: saveKey, duration: 0, style: styleNotification})
|
||||
} else if (savedRule.success) {
|
||||
if (savedPolicy.loading) {
|
||||
message.loading({ content: 'Saving...', key: saveKey, duration: 0, style: styleNotification })
|
||||
} else if (savedPolicy.success) {
|
||||
message.success({
|
||||
content: 'Rule has been successfully saved.',
|
||||
key: saveKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(ruleActions.setSetupNewRuleVisible(false))
|
||||
dispatch(ruleActions.setSavedRule({...savedRule, success: false}))
|
||||
dispatch(ruleActions.resetSavedRule(null))
|
||||
} else if (savedRule.error) {
|
||||
dispatch(policyActions.setSetupNewPolicyVisible(false))
|
||||
dispatch(policyActions.setSavedPolicy({ ...savedPolicy, success: false }))
|
||||
dispatch(policyActions.resetSavedPolicy(null))
|
||||
} else if (savedPolicy.error) {
|
||||
message.error({
|
||||
content: 'Failed to update rule. You might not have enough permissions.',
|
||||
key: saveKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(ruleActions.setSavedRule({...savedRule, error: null}))
|
||||
dispatch(ruleActions.resetSavedRule(null))
|
||||
dispatch(policyActions.setSavedPolicy({ ...savedPolicy, error: null }))
|
||||
dispatch(policyActions.resetSavedPolicy(null))
|
||||
}
|
||||
}, [savedRule])
|
||||
}, [savedPolicy])
|
||||
|
||||
const deleteKey = 'deleting';
|
||||
useEffect(() => {
|
||||
const style = {marginTop: 85}
|
||||
if (deletedRule.loading) {
|
||||
message.loading({content: 'Deleting...', key: deleteKey, style})
|
||||
} else if (deletedRule.success) {
|
||||
message.success({content: 'Rule has been successfully disabled.', key: deleteKey, duration: 2, style})
|
||||
dispatch(ruleActions.resetDeletedRule(null))
|
||||
} else if (deletedRule.error) {
|
||||
const style = { marginTop: 85 }
|
||||
if (deletedPolicy.loading) {
|
||||
message.loading({ content: 'Deleting...', key: deleteKey, style })
|
||||
} else if (deletedPolicy.success) {
|
||||
message.success({ content: 'Rule has been successfully disabled.', key: deleteKey, duration: 2, style })
|
||||
dispatch(policyActions.resetDeletedPolicy(null))
|
||||
} else if (deletedPolicy.error) {
|
||||
message.error({
|
||||
content: 'Failed to remove rule. You might not have enough permissions.',
|
||||
key: deleteKey,
|
||||
duration: 2,
|
||||
style
|
||||
})
|
||||
dispatch(ruleActions.resetDeletedRule(null))
|
||||
dispatch(policyActions.resetDeletedPolicy(null))
|
||||
}
|
||||
}, [deletedRule])
|
||||
}, [deletedPolicy])
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
@@ -193,14 +210,14 @@ export const AccessControl = () => {
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => {
|
||||
const onChangeAllEnabled = ({ target: { value } }: RadioChangeEvent) => {
|
||||
setOptionAllEnable(value)
|
||||
}
|
||||
|
||||
const showConfirmDelete = () => {
|
||||
let name = ruleToAction ? ruleToAction.name : '';
|
||||
let name = policyToAction ? policyToAction.name : '';
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: "Delete rule \"" + name + "\"",
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
@@ -208,25 +225,25 @@ export const AccessControl = () => {
|
||||
</Space>,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(ruleActions.deleteRule.request({
|
||||
dispatch(policyActions.deletePolicy.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: ruleToAction?.id || ''
|
||||
payload: policyToAction?.id || ''
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
setRuleToAction(null);
|
||||
setPolicyToAction(null);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const showConfirmDeactivate = () => {
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
{ruleToAction &&
|
||||
{policyToAction &&
|
||||
<>
|
||||
<Title level={5}>Deactivate rule "{ruleToAction ? ruleToAction.name : ''}"</Title>
|
||||
<Title level={5}>Deactivate rule "{policyToAction ? policyToAction.name : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to deactivate peer from your account?</Paragraph>
|
||||
</>
|
||||
}
|
||||
@@ -236,58 +253,78 @@ export const AccessControl = () => {
|
||||
//dispatch(ruleActions.deleteRule.request({getAccessTokenSilently, payload: ruleToAction?.id || ''}));
|
||||
},
|
||||
onCancel() {
|
||||
setRuleToAction(null);
|
||||
setPolicyToAction(null);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const filterDataTable = (): Rule[] => {
|
||||
const filterDataTable = (): Policy[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f: Rule[] = filter(rules, (f: Rule) =>
|
||||
let f: Policy[] = filter(policies, (f: Policy) =>
|
||||
(f.name.toLowerCase().includes(t) || f.description.toLowerCase().includes(t) || t === "")
|
||||
) as Rule[]
|
||||
) as Policy[]
|
||||
if (optionAllEnable !== "all") {
|
||||
f = filter(f, (f: Rule) => !f.disabled)
|
||||
f = filter(f, (f: Policy) => f.enabled)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
const onClickAddNewRule = () => {
|
||||
dispatch(ruleActions.setSetupNewRuleVisible(true));
|
||||
dispatch(ruleActions.setRule({
|
||||
const onClickAddNewPolicy = () => {
|
||||
dispatch(policyActions.setSetupNewPolicyVisible(true));
|
||||
dispatch(policyActions.setPolicy({
|
||||
name: '',
|
||||
description: '',
|
||||
sources: [],
|
||||
destinations: [],
|
||||
flow: 'bidirect',
|
||||
disabled: false
|
||||
} as Rule))
|
||||
enabled: true,
|
||||
rules: [{
|
||||
name: '',
|
||||
description: '',
|
||||
enabled: true,
|
||||
bidirectional: true,
|
||||
action: 'accept',
|
||||
protocol: 'all',
|
||||
}]
|
||||
} as Policy))
|
||||
}
|
||||
|
||||
const onClickViewRule = () => {
|
||||
dispatch(ruleActions.setSetupNewRuleVisible(true));
|
||||
dispatch(ruleActions.setRule({
|
||||
id: ruleToAction?.id || null,
|
||||
name: ruleToAction?.name,
|
||||
description: ruleToAction?.description,
|
||||
sources: ruleToAction?.sources,
|
||||
destinations: ruleToAction?.destinations,
|
||||
flow: ruleToAction?.flow,
|
||||
disabled: ruleToAction?.disabled
|
||||
} as Rule))
|
||||
const onClickViewPolicy = () => {
|
||||
dispatch(policyActions.setSetupNewPolicyVisible(true));
|
||||
dispatch(policyActions.setPolicy({
|
||||
id: policyToAction?.id || null,
|
||||
name: policyToAction?.name,
|
||||
description: policyToAction?.description,
|
||||
enabled: policyToAction?.enabled,
|
||||
rules: [{
|
||||
name: policyToAction?.name,
|
||||
description: policyToAction?.description,
|
||||
enabled: policyToAction?.enabled,
|
||||
sources: policyToAction?.sources,
|
||||
destinations: policyToAction?.destinations,
|
||||
bidirectional: policyToAction?.bidirectional,
|
||||
protocol: policyToAction?.protocol,
|
||||
ports: policyToAction?.ports,
|
||||
}]
|
||||
} as Policy))
|
||||
}
|
||||
|
||||
const setRuleAndView = (rule: RuleDataTable) => {
|
||||
dispatch(ruleActions.setSetupNewRuleVisible(true));
|
||||
dispatch(ruleActions.setRule({
|
||||
id: rule.id || null,
|
||||
name: rule.name,
|
||||
description: rule.description,
|
||||
sources: rule.sources,
|
||||
destinations: rule.destinations,
|
||||
flow: rule.flow,
|
||||
disabled: rule.disabled
|
||||
} as Rule))
|
||||
const setPolicyAndView = (p: PolicyDataTable) => {
|
||||
dispatch(policyActions.setSetupNewPolicyVisible(true));
|
||||
dispatch(policyActions.setPolicy({
|
||||
id: p.id || null,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
enabled: p.enabled,
|
||||
rules: [{
|
||||
id: p.id || null,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
enabled: p.enabled,
|
||||
sources: p.sources,
|
||||
destinations: p.destinations,
|
||||
bidirectional: p.bidirectional,
|
||||
protocol: p.protocol,
|
||||
ports: p.ports,
|
||||
}]
|
||||
} as Policy))
|
||||
}
|
||||
|
||||
const toggleModalGroups = (title: string, groups: Group[] | string[] | null, modalVisible: boolean) => {
|
||||
@@ -299,13 +336,13 @@ export const AccessControl = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (setupNewRuleVisible) {
|
||||
if (setupNewPolicyVisible) {
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}, [setupNewRuleVisible])
|
||||
}, [setupNewPolicyVisible])
|
||||
|
||||
const onPopoverVisibleChange = (b: boolean, key: string) => {
|
||||
if (setupNewRuleVisible) {
|
||||
if (setupNewPolicyVisible) {
|
||||
setGroupPopupVisible("")
|
||||
} else {
|
||||
if (b) {
|
||||
@@ -316,20 +353,20 @@ export const AccessControl = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, groups: Group[] | string[] | null, rule: RuleDataTable) => {
|
||||
const renderPopoverGroups = (label: string, groups: Group[] | string[] | null, rule: PolicyDataTable) => {
|
||||
const content = groups?.map((g, i) => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{ fontSize: ".85em" }}>{peersCount}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
@@ -339,11 +376,20 @@ export const AccessControl = () => {
|
||||
open={groupPopupVisible === rule.key}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
<Button type="link" onClick={() => setRuleAndView(rule)}>{label}</Button>
|
||||
<Button type="link" onClick={() => setPolicyAndView(rule)}>{label}</Button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const renderPorts = (ports: string[]) => {
|
||||
const content = ports?.map((p, i) => {
|
||||
return (
|
||||
<Tag key={i} style={{ marginRight: 3 }}><strong>{p}</strong></Tag>
|
||||
)
|
||||
})
|
||||
return (<div>{content}</div>)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container className="container-main">
|
||||
@@ -351,11 +397,11 @@ export const AccessControl = () => {
|
||||
<Col span={24}>
|
||||
<Title level={4}>Access Control</Title>
|
||||
<Paragraph>Access rules help you manage access permissions in your organisation.</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Space direction="vertical" size="large" style={{ display: 'flex' }}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
placeholder="Search..." onChange={onChangeTextToSearch} />
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
@@ -367,28 +413,28 @@ export const AccessControl = () => {
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en" />
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5} span={5}>
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button type="primary" disabled={savedRule.loading}
|
||||
onClick={onClickAddNewRule}>Add Rule</Button>
|
||||
<Button type="primary" disabled={savedPolicy.loading}
|
||||
onClick={onClickAddNewPolicy}>Add Rule</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.message : " "} type="error" showIcon
|
||||
closable/>
|
||||
closable />
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Card bodyStyle={{ padding: 0 }}>
|
||||
<Table
|
||||
pagination={{
|
||||
current: currentPage, hideOnSinglePage: showTutorial, disabled: showTutorial,
|
||||
@@ -400,74 +446,82 @@ export const AccessControl = () => {
|
||||
}}
|
||||
className={`access-control-table ${showTutorial ? "card-table card-table-no-placeholder" : "card-table"}`}
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
scroll={{ x: true }}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Name" dataIndex="name"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}
|
||||
defaultSortOrder='ascend'
|
||||
render={(text, record, index) => {
|
||||
const desc = (record as RuleDataTable).description.trim()
|
||||
return <Tooltip title={desc !== "" ? desc : "no description"}
|
||||
arrowPointAtCenter>
|
||||
<span onClick={() => setRuleAndView(record as RuleDataTable)}
|
||||
className="tooltip-label"><Text strong>{text}</Text></span>
|
||||
</Tooltip>
|
||||
}}
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}
|
||||
defaultSortOrder='ascend'
|
||||
render={(text, record, index) => {
|
||||
const desc = (record as PolicyDataTable).description.trim()
|
||||
return <Tooltip title={desc !== "" ? desc : "no description"}
|
||||
arrowPointAtCenter>
|
||||
<span onClick={() => setPolicyAndView(record as PolicyDataTable)}
|
||||
className="tooltip-label"><Text strong>{text}</Text></span>
|
||||
</Tooltip>
|
||||
}}
|
||||
/>
|
||||
<Column title="Status" dataIndex="disabled"
|
||||
render={(text: Boolean, record: RuleDataTable, index) => {
|
||||
return text ? <Tag color="red">disabled</Tag> :
|
||||
<Tag color="green">enabled</Tag>
|
||||
}}
|
||||
<Column title="Status" dataIndex="enabled"
|
||||
render={(text: Boolean, record: PolicyDataTable, index) => {
|
||||
return text ? <Tag color="green">enabled</Tag> : <Tag color="red">disabled</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Sources" dataIndex="sourceLabel"
|
||||
render={(text, record: RuleDataTable, index) => {
|
||||
//return <Button type="link" onClick={() => toggleModalGroups(`${record.Name} - Sources`, record.Source, true)}>{text}</Button>
|
||||
return renderPopoverGroups(text, record.sources, record as RuleDataTable)
|
||||
}}
|
||||
render={(text, record: PolicyDataTable, index) => {
|
||||
//return <Button type="link" onClick={() => toggleModalGroups(`${record.Name} - Sources`, record.Source, true)}>{text}</Button>
|
||||
return renderPopoverGroups(text, record.sources, record as PolicyDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Direction" dataIndex="flow"
|
||||
render={(text, record: RuleDataTable, index) => {
|
||||
const s = {minWidth: 50, textAlign: "center"} as React.CSSProperties
|
||||
if (text === "bidirect")
|
||||
return <Tag color="processing" style={s}><img src={bidirect}/></Tag>
|
||||
else if (text === "srcToDest") {
|
||||
return <Tag color="green" style={s}><img src={outbound}/></Tag>
|
||||
} else if (text === "destToSrc") {
|
||||
return <Tag color="green" style={s}><img src={inbound}/></Tag>
|
||||
}
|
||||
return <Tag color="red" style={s}><CloseOutlined/></Tag>
|
||||
}}
|
||||
<Column title="Direction" dataIndex="bidirectional"
|
||||
render={(text, record: PolicyDataTable, index) => {
|
||||
const s = { minWidth: 50, textAlign: "center" } as React.CSSProperties
|
||||
if (record.bidirectional) {
|
||||
return <Tag color="processing" style={s}><img src={bidirect} /></Tag>
|
||||
}
|
||||
return <Tag color="green" style={s}><img src={outbound} /></Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Destinations" dataIndex="destinationLabel"
|
||||
render={(text, record: RuleDataTable, index) => {
|
||||
//return <Button type="link" onClick={() => toggleModalGroups(`${record.name} - Destinations`, record.destinations, true)}>{text}</Button>
|
||||
return renderPopoverGroups(text, record.destinations, record as RuleDataTable)
|
||||
}}
|
||||
render={(text, record: PolicyDataTable, index) => {
|
||||
//return <Button type="link" onClick={() => toggleModalGroups(`${record.name} - Destinations`, record.destinations, true)}>{text}</Button>
|
||||
return renderPopoverGroups(text, record.destinations, record as PolicyDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Protocol" dataIndex="protocol"
|
||||
render={(text, record: PolicyDataTable, index) => {
|
||||
return <Tag
|
||||
style={{ marginRight: "3", textTransform: "uppercase" }}>
|
||||
{record.protocol}
|
||||
</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Ports" dataIndex="ports"
|
||||
render={(text, record: PolicyDataTable, index) => {
|
||||
return renderPorts(record.ports)
|
||||
}}
|
||||
/>
|
||||
<Column title="" align="center"
|
||||
render={(text, record, index) => {
|
||||
if (deletedRule.loading || savedRule.loading) return <></>
|
||||
return (
|
||||
<Dropdown trigger={["click"]} overlay={actionsMenu} onOpenChange={visible => {
|
||||
if (visible) setRuleToAction(record as RuleDataTable)
|
||||
}}>
|
||||
<Button type="text">
|
||||
<Space>
|
||||
<EllipsisOutlined />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)
|
||||
}}
|
||||
render={(text, record, index) => {
|
||||
if (deletedPolicy.loading || savedPolicy.loading) return <></>
|
||||
return (
|
||||
<Dropdown trigger={["click"]} overlay={actionsMenu} onOpenChange={visible => {
|
||||
if (visible) setPolicyToAction(record as PolicyDataTable)
|
||||
}}>
|
||||
<Button type="text">
|
||||
<Space>
|
||||
<EllipsisOutlined />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
{showTutorial &&
|
||||
<Space direction="vertical" size="small" align="center"
|
||||
style={{display: 'flex', padding: '45px 15px'}}>
|
||||
<Button type="link" onClick={onClickAddNewRule}>Add new access rule</Button>
|
||||
style={{ display: 'flex', padding: '45px 15px' }}>
|
||||
<Button type="link" onClick={onClickAddNewPolicy}>Add new access rule</Button>
|
||||
</Space>
|
||||
}
|
||||
</Card>
|
||||
@@ -476,11 +530,11 @@ export const AccessControl = () => {
|
||||
</Row>
|
||||
</Container>
|
||||
<AccessControlModalGroups data={groupsToShow.groups} title={groupsToShow.title}
|
||||
visible={groupsToShow.modalVisible}
|
||||
onCancel={() => toggleModalGroups("", [], false)}/>
|
||||
<AccessControlNew/>
|
||||
visible={groupsToShow.modalVisible}
|
||||
onCancel={() => toggleModalGroups("", [], false)} />
|
||||
<AccessControlNew />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccessControl;
|
||||
export default AccessControl;
|
||||
|
||||
Reference in New Issue
Block a user