mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
DNS (#101)
Added DNS tab for managing Nameservers. Users will be able to add multiple nameservers and set distribution groups that dictate to which peers the settings will be applied. With this PR we also got a set of group handlers that can be reused.
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -34,6 +34,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"postcss": "^8.4.12",
|
||||
"prop-types": "^15.7.2",
|
||||
"punycode": "^2.1.1",
|
||||
"rc-overflow": "^1.2.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.1.0",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"postcss": "^8.4.12",
|
||||
"prop-types": "^15.7.2",
|
||||
"punycode": "^2.1.1",
|
||||
"rc-overflow": "^1.2.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.1.0",
|
||||
|
||||
82
src/App.tsx
82
src/App.tsx
@@ -19,6 +19,9 @@ import FooterComponent from "./components/FooterComponent";
|
||||
import {useGetAccessTokenSilently} from "./utils/token";
|
||||
import {User} from "./store/user/types";
|
||||
import {SecureLoading} from "./components/Loading";
|
||||
import DNS from "./views/DNS";
|
||||
|
||||
|
||||
|
||||
const {Header, Content} = Layout;
|
||||
|
||||
@@ -52,8 +55,8 @@ function App() {
|
||||
if (!run.current) {
|
||||
run.current = true
|
||||
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getAccessTokenSilently})
|
||||
.then((resp) => {
|
||||
setShow(true)
|
||||
.then(() => {
|
||||
setShow(true)
|
||||
})
|
||||
.catch(e => {
|
||||
setShow(true)
|
||||
@@ -68,43 +71,44 @@ function App() {
|
||||
<Provider store={store}>
|
||||
{!show && <SecureLoading padding="3em" width={50} height={50}/>}
|
||||
{show &&
|
||||
<Layout>
|
||||
<Banner/>
|
||||
<Header className="header" style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
alignContent: "center"
|
||||
}}>
|
||||
<Row justify="space-around" align="middle">
|
||||
<Col span={24}>
|
||||
<Container>
|
||||
<Navbar/>
|
||||
</Container>
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
<Content style={{minHeight: "100vh"}}>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => {
|
||||
return (
|
||||
<Redirect to="/peers"/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route path='/peers' exact component={withOidcSecure(Peers)}/>
|
||||
<Route path="/add-peer" component={withOidcSecure(AddPeer)}/>
|
||||
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
|
||||
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
|
||||
<Route path="/routes" component={withOidcSecure(Routes)}/>
|
||||
<Route path="/users" component={withOidcSecure(Users)}/>
|
||||
</Switch>
|
||||
</Content>
|
||||
<FooterComponent/>
|
||||
</Layout>
|
||||
<Layout>
|
||||
<Banner/>
|
||||
<Header className="header" style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
alignContent: "center"
|
||||
}}>
|
||||
<Row justify="space-around" align="middle">
|
||||
<Col span={24}>
|
||||
<Container>
|
||||
<Navbar/>
|
||||
</Container>
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
<Content style={{minHeight: "100vh"}}>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => {
|
||||
return (
|
||||
<Redirect to="/peers"/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route path='/peers' exact component={withOidcSecure(Peers)}/>
|
||||
<Route path="/add-peer" component={withOidcSecure(AddPeer)}/>
|
||||
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
|
||||
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
|
||||
<Route path="/routes" component={withOidcSecure(Routes)}/>
|
||||
<Route path="/users" component={withOidcSecure(Users)}/>
|
||||
<Route path="/dns" component={withOidcSecure(DNS)}/>
|
||||
</Switch>
|
||||
</Content>
|
||||
<FooterComponent/>
|
||||
</Layout>
|
||||
}
|
||||
</Provider>
|
||||
</>
|
||||
|
||||
@@ -228,7 +228,7 @@ const AccessControlNew = () => {
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
if (!value.length) {
|
||||
return Promise.reject(new Error("Please enter ate least one group"))
|
||||
return Promise.reject(new Error("Please enter at least one group"))
|
||||
}
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
|
||||
616
src/components/NameServerGroupUpdate.tsx
Normal file
616
src/components/NameServerGroupUpdate.tsx
Normal file
@@ -0,0 +1,616 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as nsGroupActions} from '../store/nameservers';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
FormListFieldData,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "antd";
|
||||
import {CloseOutlined, FlagFilled, MinusCircleOutlined, PlusOutlined, QuestionCircleFilled, QuestionCircleOutlined} from "@ant-design/icons";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import cidrRegex from 'cidr-regex';
|
||||
import {NameServer, NameServerGroup, NameServerGroupToSave} from "../store/nameservers/types";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups"
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
|
||||
interface formNSGroup extends NameServerGroup {
|
||||
}
|
||||
|
||||
const NameServerGroupUpdate = () => {
|
||||
const {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator
|
||||
} = useGetGroupTagHelpers()
|
||||
const dispatch = useDispatch()
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {Option} = Select;
|
||||
const nsGroup = useSelector((state: RootState) => state.nameserverGroup.nameserverGroup)
|
||||
const setupNewNameServerGroupVisible = useSelector((state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible)
|
||||
const savedNSGroup = useSelector((state: RootState) => state.nameserverGroup.savedNameServerGroup)
|
||||
const nsGroupData = useSelector((state: RootState) => state.nameserverGroup.data);
|
||||
|
||||
const [formNSGroup, setFormNSGroup] = useState({} as formNSGroup)
|
||||
const [form] = Form.useForm()
|
||||
const [editName, setEditName] = useState(false)
|
||||
const [isPrimary, setIsPrimary] = useState(false)
|
||||
const [editDescription, setEditDescription] = useState(false)
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const inputDescriptionRef = useRef<any>(null)
|
||||
const [selectCustom, setSelectCustom] = useState(false)
|
||||
|
||||
const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]
|
||||
const optionsPrimary = [{label: 'Yes', value: true}, {label: 'No', value: false}]
|
||||
|
||||
useEffect(() => {
|
||||
if (!nsGroup) return
|
||||
|
||||
let newFormGroup = {
|
||||
...nsGroup,
|
||||
groups: getGroupNamesFromIDs(nsGroup.groups),
|
||||
} as formNSGroup
|
||||
setFormNSGroup(newFormGroup)
|
||||
form.setFieldsValue(newFormGroup)
|
||||
if (nsGroup.id) {
|
||||
setSelectCustom(true)
|
||||
}
|
||||
if (nsGroup.primary !== undefined) {
|
||||
setIsPrimary(nsGroup.primary)
|
||||
}
|
||||
}, [nsGroup])
|
||||
|
||||
const onCancel = () => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(false));
|
||||
dispatch(nsGroupActions.setNameServerGroup(
|
||||
{
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
primary: false,
|
||||
domains: [],
|
||||
nameservers: [] as NameServer[],
|
||||
groups: [],
|
||||
enabled: false,
|
||||
} as NameServerGroup
|
||||
))
|
||||
setEditName(false)
|
||||
setSelectCustom(false)
|
||||
setIsPrimary(false)
|
||||
}
|
||||
|
||||
const onChange = (changedValues:any) => {
|
||||
if (changedValues.primary !== undefined) {
|
||||
setIsPrimary(changedValues.primary)
|
||||
}
|
||||
}
|
||||
|
||||
let googleChoice = 'Google DNS'
|
||||
let cloudflareChoice = 'Cloudflare DNS'
|
||||
let quad9Choice = 'Quad9 DNS'
|
||||
let customChoice = 'Add custom nameserver'
|
||||
|
||||
let defaultDNSOptions: NameServerGroup[] = [
|
||||
{
|
||||
name: googleChoice,
|
||||
description: 'Google DNS servers',
|
||||
domains: [],
|
||||
primary: true,
|
||||
nameservers: [
|
||||
{
|
||||
ip: "8.8.8.8",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
{
|
||||
ip: "8.8.4.4",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: cloudflareChoice,
|
||||
description: 'Cloudflare DNS servers',
|
||||
domains: [],
|
||||
primary: true,
|
||||
nameservers: [
|
||||
{
|
||||
ip: "1.1.1.1",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
{
|
||||
ip: "1.0.0.1",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: quad9Choice,
|
||||
description: 'Quad9 DNS servers',
|
||||
domains: [],
|
||||
primary: true,
|
||||
nameservers: [
|
||||
{
|
||||
ip: "9.9.9.9",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
{
|
||||
ip: "149.112.112.112",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
enabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
const handleSelectChange = (value: string) => {
|
||||
console.log(`selected ${value}`);
|
||||
let nsGroupLocal = {} as NameServerGroup
|
||||
if (value === customChoice) {
|
||||
nsGroupLocal = nsGroup
|
||||
} else {
|
||||
defaultDNSOptions.forEach((nsg) => {
|
||||
if (value === nsg.name) {
|
||||
nsGroupLocal = nsg
|
||||
}
|
||||
})
|
||||
}
|
||||
let newFormGroup = {
|
||||
...nsGroupLocal,
|
||||
groups: getGroupNamesFromIDs(nsGroupLocal.groups),
|
||||
} as formNSGroup
|
||||
setFormNSGroup(newFormGroup)
|
||||
form.setFieldsValue(newFormGroup)
|
||||
setSelectCustom(true)
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
const nsGroupToSave = createNSGroupToSave(values as NameServerGroup)
|
||||
dispatch(nsGroupActions.saveNameServerGroup.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: nsGroupToSave
|
||||
}))
|
||||
|
||||
})
|
||||
.then(() => onCancel())
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
}
|
||||
|
||||
const createNSGroupToSave = (values:NameServerGroup): NameServerGroupToSave => {
|
||||
let [existingGroups, newGroups] = getExistingAndToCreateGroupsLists(values.groups)
|
||||
return {
|
||||
id: formNSGroup.id || null,
|
||||
name: values.name ? values.name : formNSGroup.name,
|
||||
description: values.description ? values.description : formNSGroup.description,
|
||||
primary: values.primary,
|
||||
domains: values.primary ? [] : values.domains,
|
||||
nameservers: values.nameservers,
|
||||
groups: existingGroups,
|
||||
groupsToCreate: newGroups,
|
||||
enabled: values.enabled,
|
||||
} as NameServerGroupToSave
|
||||
}
|
||||
|
||||
const toggleEditName = (b: boolean) => {
|
||||
setEditDescription(b)
|
||||
}
|
||||
|
||||
const toggleEditDescription = (b: boolean) => {
|
||||
setEditDescription(b)
|
||||
}
|
||||
|
||||
const domainRegex = /(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/;
|
||||
|
||||
const domainValidator = (_: RuleObject, domain: string) => {
|
||||
if (domainRegex.test(domain)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.reject(new Error("Please enter a valid domain, e.g. example.com or intra.example.com"))
|
||||
}
|
||||
|
||||
const nameValidator = (_: RuleObject, value: string) => {
|
||||
const found = nsGroupData.find(u => u.name == value && u.id !== formNSGroup.id)
|
||||
if (found) {
|
||||
return Promise.reject(new Error("Please enter a unique name for your nameserver configuration"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const ipValidator = (_: RuleObject, value: string) => {
|
||||
if (!cidrRegex().test(value + "/32")) {
|
||||
return Promise.reject(new Error("Please enter a valid IP, e.g. 192.168.1.1 or 8.8.8.8"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const formListValidator = (_: RuleObject, names) => {
|
||||
if (names.length >= 3) {
|
||||
return Promise.reject(new Error("Exceeded maximum number of Nameservers. (Max is 2)"));
|
||||
}
|
||||
if (names.length < 1) {
|
||||
return Promise.reject(new Error("You should add at least 1 Nameserver"));
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const renderNSList = (fields: FormListFieldData[], {add, remove}, {errors}) => (
|
||||
<>
|
||||
<Row>Nameservers</Row>
|
||||
{!!fields.length && (
|
||||
|
||||
<Row align='middle'>
|
||||
<Col span={6} style={{textAlign: 'center'}}>
|
||||
<Typography.Text>Protocol</Typography.Text>
|
||||
</Col>
|
||||
<Col span={10} style={{textAlign: 'center'}}>
|
||||
<Typography.Text>Nameserver IP</Typography.Text>
|
||||
</Col>
|
||||
<Col span={4} style={{textAlign: 'center'}}>
|
||||
<Typography.Text>Port</Typography.Text>
|
||||
</Col>
|
||||
<Col span={2}/>
|
||||
</Row>
|
||||
)}
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<Row key={index}>
|
||||
<Col span={6} style={{textAlign: 'center'}}>
|
||||
<Form.Item style={{margin: '3px'}}
|
||||
name={[field.name, 'ns_type']}
|
||||
rules={[{required: true, message: 'Missing first protocol'}]}
|
||||
initialValue={"udp"}
|
||||
>
|
||||
<Select disabled style={{width: '100%'}}>
|
||||
<Option value="udp">UDP</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10} style={{margin: '1px'}}>
|
||||
<Form.Item style={{margin: '1px'}}
|
||||
name={[field.name, 'ip']}
|
||||
rules={[{validator: ipValidator}]}
|
||||
>
|
||||
<Input placeholder="e.g. X.X.X.X" style={{width: '100%'}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4} style={{textAlign: 'center'}}>
|
||||
<Form.Item style={{margin: '1px'}}
|
||||
name={[field.name, 'port']}
|
||||
rules={[{required: true, message: 'Missing port'}]}
|
||||
initialValue={53}
|
||||
>
|
||||
<InputNumber placeholder="Port" style={{width: '100%'}}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2} style={{textAlign: 'center'}}>
|
||||
<MinusCircleOutlined onClick={() => remove(field.name)}/>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
})}
|
||||
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined/>}>
|
||||
Add nameserver
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</>
|
||||
)
|
||||
|
||||
// @ts-ignore
|
||||
const renderDomains = (fields: FormListFieldData[], {add, remove}, {errors}) => (
|
||||
<>
|
||||
<Row>
|
||||
<Space >
|
||||
<Col>
|
||||
Match domains
|
||||
</Col>
|
||||
<Col>
|
||||
<Tooltip title="Only queries to domains specified here will be resolved by these nameservers." className={"ant-form-item-tooltip"}>
|
||||
<QuestionCircleOutlined style={{color: "rgba(0, 0, 0, 0.45)",cursor: "help"}}/>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
</Space>
|
||||
</Row>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<Row key={index}>
|
||||
<Col span={20} style={{margin: '1px'}}>
|
||||
<Form.Item hidden={isPrimary} style={{margin: '1px'}}
|
||||
{...field}
|
||||
rules={[{validator: domainValidator}]}
|
||||
>
|
||||
<Input placeholder="e.g. example.com" style={{width: '100%'}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2} style={{textAlign: 'center'}}>
|
||||
<MinusCircleOutlined hidden={isPrimary} className="dynamic-delete-button" onClick={() => remove(field.name)}/>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
})}
|
||||
|
||||
<Form.Item>
|
||||
<Button type="dashed" disabled={isPrimary} onClick={() => add()} block icon={<PlusOutlined/>}>
|
||||
Add domain
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{nsGroup &&
|
||||
<Drawer
|
||||
headerStyle={{display: "none"}}
|
||||
forceRender={true}
|
||||
open={setupNewNameServerGroupVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
autoFocus={true}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedNSGroup.loading}>Cancel</Button>
|
||||
<Button type="primary" onClick={handleFormSubmit} disabled={savedNSGroup.loading}
|
||||
>{`${formNSGroup.id ? 'Save' : 'Create'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{selectCustom ?
|
||||
(<Form layout="vertical" requiredMark={false} form={form}
|
||||
onValuesChange={onChange}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
<Row align="top">
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && !editDescription && formNSGroup.id &&
|
||||
<button type="button" aria-label="Close"
|
||||
className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close"
|
||||
className="anticon anticon-close">
|
||||
<CloseOutlined size={16}/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
{!editName && formNSGroup.id ? (
|
||||
<div className={"access-control input-text ant-drawer-title"}
|
||||
onClick={() => toggleEditName(true)}>{formNSGroup.id ? formNSGroup.name : 'New nameserver group'}</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
tooltip="Add a nameserver group name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please add an identifier for this nameserver group',
|
||||
whitespace: true
|
||||
},
|
||||
{
|
||||
validator: nameValidator
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input placeholder="e.g. Public DNS" ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)} autoComplete="off"
|
||||
maxLength={40}/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{!editDescription ? (
|
||||
<div className={"access-control input-text ant-drawer-subtitle"}
|
||||
onClick={() => toggleEditDescription(true)}>{formNSGroup.description && formNSGroup.description.trim() !== "" ? formNSGroup.description : 'Add description...'}</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{marginTop: 24}}
|
||||
>
|
||||
<Input placeholder="Add description..."
|
||||
ref={inputDescriptionRef}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off" maxLength={200}/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="top">
|
||||
<Col flex="auto">
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Header>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="enabled"
|
||||
label="Status"
|
||||
>
|
||||
<Radio.Group
|
||||
options={optionsDisabledEnabled}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24} flex="auto">
|
||||
<Form.List
|
||||
name="nameservers"
|
||||
rules={[{validator: formListValidator}]}
|
||||
>
|
||||
{renderNSList}
|
||||
</Form.List>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="primary"
|
||||
label="Resolve all domains"
|
||||
tooltip="Defines if the nameservers are resolvers for all domains"
|
||||
>
|
||||
<Radio.Group
|
||||
options={optionsPrimary}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24} flex="auto">
|
||||
<Form.List
|
||||
name="domains"
|
||||
>
|
||||
{renderDomains}
|
||||
</Form.List>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label="Distribution groups"
|
||||
tooltip="Distribution groups define to which group of peers these settings will be distributed to"
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the NS group"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
|
||||
<Col span={24}>
|
||||
<Row wrap={false} gutter={12}>
|
||||
<Col flex="none">
|
||||
<FlagFilled/>
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<Paragraph>
|
||||
Nameservers let you define resolvers for your DNS queries.
|
||||
Because not all operating systems support match-only domain resolution,
|
||||
you should define at least one set of nameservers to resolve all domains per distribution group.
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/how-to-guides/nameservers"
|
||||
style={{color: 'rgb(07, 114, 128)'}}>Learn
|
||||
more about nameservers</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>) :
|
||||
(
|
||||
<Space direction={"vertical"} style={{ width: '100%' }}>
|
||||
<Row align='middle'>
|
||||
<Col span={24} style={{textAlign: 'left'}}>
|
||||
<span className="ant-form-item">Select a predefined nameserver</span>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align='middle'>
|
||||
<Col span={24} style={{textAlign: 'center'}}>
|
||||
<Select
|
||||
style={{width: '100%'}}
|
||||
onChange={handleSelectChange}
|
||||
options={[
|
||||
{
|
||||
value: googleChoice,
|
||||
label: googleChoice,
|
||||
},
|
||||
{
|
||||
value: cloudflareChoice,
|
||||
label: cloudflareChoice,
|
||||
},
|
||||
{
|
||||
value: quad9Choice,
|
||||
label: quad9Choice,
|
||||
},
|
||||
{
|
||||
value: customChoice,
|
||||
label: customChoice,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align='middle'>
|
||||
<Col span={24} style={{textAlign: 'left'}}>
|
||||
<Col span={24} style={{textAlign: 'left'}}>
|
||||
<span className="ant-form-item"><Typography.Link onClick={() => handleSelectChange(customChoice)}>Or add custom</Typography.Link></span>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
</Drawer>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NameServerGroupUpdate
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Link, useLocation} from 'react-router-dom';
|
||||
import logo from "../assets/logo.png";
|
||||
import {Avatar, Button, Col, Dropdown, Grid, Menu, Row, Typography} from 'antd'
|
||||
import {Avatar, Button, Col, Dropdown, Grid, Menu, Row} from 'antd'
|
||||
import {ItemType} from "antd/lib/menu/hooks/useItems";
|
||||
import {AvatarSize} from "antd/es/avatar/SizeContext";
|
||||
import {UserOutlined} from '@ant-design/icons';
|
||||
@@ -11,21 +11,12 @@ import {User} from "../store/user/types";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
|
||||
const {Text} = Typography
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
interface NavbarProps {
|
||||
users: User[]
|
||||
}
|
||||
|
||||
|
||||
const Navbar = () => {
|
||||
let location = useLocation();
|
||||
const config = getConfig();
|
||||
const {
|
||||
isAuthenticated,
|
||||
logout,
|
||||
} = useOidc();
|
||||
const { logout } = useOidc();
|
||||
|
||||
const {oidcUser} = useOidcUser();
|
||||
const user = oidcUser;
|
||||
@@ -42,13 +33,14 @@ const Navbar = () => {
|
||||
{label: (<Link to="/setup-keys">Setup Keys</Link>), key: '/setup-keys'},
|
||||
{label: (<Link to="/acls">Access Control</Link>), key: '/acls'},
|
||||
{label: (<Link to="/routes">Network Routes</Link>), key: '/routes'},
|
||||
{ label: (<Link to="/dns">DNS</Link>), key: '/dns' },
|
||||
{label: (<Link to="/users">Users</Link>), key: '/users'}
|
||||
] as ItemType[]
|
||||
|
||||
const userEmailKey = 'user-email'
|
||||
const userLogoutKey = 'user-logout'
|
||||
const userDividerKey = 'user-divider'
|
||||
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes"]
|
||||
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns"]
|
||||
const [menuItems, setMenuItems] = useState(items)
|
||||
const logoutWithRedirect = () =>
|
||||
logout("/", {client_id: config.clientId});
|
||||
@@ -82,10 +74,8 @@ const Navbar = () => {
|
||||
if (found) {
|
||||
setCurrentUser(found)
|
||||
}
|
||||
} else {
|
||||
setCurrentUser({} as User)
|
||||
}
|
||||
}, [users, user])
|
||||
}, [users, oidcUser])
|
||||
|
||||
const showTab = (key: string | undefined, user: User | undefined) => {
|
||||
if (!user) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {timeAgo} from "../utils/common";
|
||||
const {Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
const {Panel} = Collapse;
|
||||
const punycode = require('punycode/')
|
||||
|
||||
const PeerUpdate = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
@@ -32,6 +33,7 @@ const PeerUpdate = () => {
|
||||
const [peerGroups, setPeerGroups] = useState([] as GroupPeer[])
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const [editName, setEditName] = useState(false)
|
||||
const [estimatedName, setEstimatedName] = useState("")
|
||||
const [callingPeerAPI, setCallingPeerAPI] = useState(false)
|
||||
const [callingGroupAPI, setCallingGroupAPI] = useState(false)
|
||||
const [isSubmitRunning, setSubmitRunning] = useState(false)
|
||||
@@ -212,6 +214,20 @@ const PeerUpdate = () => {
|
||||
setSelectedTagGroups(validatedValues)
|
||||
};
|
||||
|
||||
const nameValidator = (_: RuleObject, value: string) => {
|
||||
let punyName = punycode.toASCII(value.toLowerCase())
|
||||
let domain = ""
|
||||
if (formPeer.dns_label) {
|
||||
let labelList = formPeer.dns_label.split(".")
|
||||
if (labelList.length > 1) {
|
||||
labelList.splice(0,1)
|
||||
domain = "." + labelList.join(".")
|
||||
}
|
||||
}
|
||||
setEstimatedName(punyName+domain)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const createPeerToSave = (): Peer => {
|
||||
return {
|
||||
id: formPeer.id,
|
||||
@@ -316,22 +332,42 @@ const PeerUpdate = () => {
|
||||
onClick={() => toggleEditName(true)}>{formPeer.name ? formPeer.name : peer.name}
|
||||
<EditOutlined/></div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a new name for this peer',
|
||||
whitespace: true
|
||||
}]}
|
||||
>
|
||||
<Input
|
||||
placeholder={peer.name}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>)}
|
||||
<Row>
|
||||
<Space direction={"vertical"} size="small">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
style={{margin: '1px'}}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a new name for this peer',
|
||||
whitespace: true
|
||||
},{validator:nameValidator}]}
|
||||
>
|
||||
<Input
|
||||
placeholder={peer.name}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"
|
||||
max={59}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Possible domain name after saving"
|
||||
tooltip="If the domain name already exists, we add an increment number suffix to it"
|
||||
style={{margin: '1px'}}
|
||||
>
|
||||
<Paragraph>
|
||||
<Tag>
|
||||
{estimatedName}
|
||||
</Tag>
|
||||
</Paragraph>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -370,6 +406,20 @@ const PeerUpdate = () => {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="dns_label"
|
||||
label="Domain name"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.userEmail}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
{formPeer.user_id && (
|
||||
<Col span={24}>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { sagas as userSagas } from './user';
|
||||
import { sagas as ruleSagas } from './rule';
|
||||
import { sagas as groupSagas } from './group';
|
||||
import { sagas as routeSagas } from './route';
|
||||
import { sagas as nameserverGroupSagas } from './nameservers';
|
||||
|
||||
import rootReducer from './root-reducer';
|
||||
import { apiClient } from '../services/api-client';
|
||||
@@ -25,5 +26,6 @@ sagaMiddleware.run(userSagas);
|
||||
sagaMiddleware.run(ruleSagas);
|
||||
sagaMiddleware.run(groupSagas);
|
||||
sagaMiddleware.run(routeSagas);
|
||||
sagaMiddleware.run(nameserverGroupSagas);
|
||||
|
||||
export { apiClient, rootReducer, store };
|
||||
35
src/store/nameservers/actions.ts
Normal file
35
src/store/nameservers/actions.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
|
||||
import {NameServerGroup, NameServerGroupToSave} from './types';
|
||||
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getNameServerGroups: createAsyncAction(
|
||||
'GET_NameServerGroup_REQUEST',
|
||||
'GET_NameServerGroup_SUCCESS',
|
||||
'GET_NameServerGroup_FAILURE',
|
||||
)<RequestPayload<null>, NameServerGroup[], ApiError>(),
|
||||
|
||||
saveNameServerGroup: createAsyncAction(
|
||||
'SAVE_NameServerGroup_REQUEST',
|
||||
'SAVE_NameServerGroup_SUCCESS',
|
||||
'SAVE_NameServerGroup_FAILURE',
|
||||
)<RequestPayload<NameServerGroupToSave>, CreateResponse<NameServerGroup | null>, CreateResponse<NameServerGroup | null>>(),
|
||||
setSavedNameServerGroup: createAction('SET_CREATE_NameServerGroup')<CreateResponse<NameServerGroup | null>>(),
|
||||
resetSavedNameServerGroup: createAction('RESET_CREATE_NameServerGroup')<null>(),
|
||||
|
||||
deleteNameServerGroup: createAsyncAction(
|
||||
'DELETE_NameServerGroup_REQUEST',
|
||||
'DELETE_NameServerGroup_SUCCESS',
|
||||
'DELETE_NameServerGroup_FAILURE'
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
setDeletedNameServerGroup: createAction('SET_DELETED_NameServerGroup')<DeleteResponse<string | null>>(),
|
||||
resetDeletedNameServerGroup: createAction('RESET_DELETED_NameServerGroup')<null>(),
|
||||
removeNameServerGroup: createAction('REMOVE_NameServerGroup')<string>(),
|
||||
|
||||
setNameServerGroup: createAction('SET_NameServerGroup')<NameServerGroup>(),
|
||||
setSetupNewNameServerGroupVisible: createAction('SET_SETUP_NEW_NameServerGroup_VISIBLE')<boolean>(),
|
||||
setSetupNewNameServerGroupHA: createAction('SET_SETUP_NEW_NameServerGroup_HA')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
export default actions;
|
||||
7
src/store/nameservers/index.ts
Normal file
7
src/store/nameservers/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 };
|
||||
95
src/store/nameservers/reducer.ts
Normal file
95
src/store/nameservers/reducer.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { NameServerGroup } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, DeleteResponse, CreateResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: NameServerGroup[] | null;
|
||||
nameserverGroup: NameServerGroup | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
deleteNameServerGroup: DeleteResponse<string | null>;
|
||||
savedNameServerGroup: CreateResponse<NameServerGroup | null>;
|
||||
setupNewNameServerGroupVisible: boolean;
|
||||
setupNewNameServerGroupHA: boolean
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
nameserverGroup: null,
|
||||
loading: false,
|
||||
failed: null,
|
||||
saving: false,
|
||||
deleteNameServerGroup: <DeleteResponse<string | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
savedNameServerGroup: <CreateResponse<NameServerGroup | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
setupNewNameServerGroupVisible: false,
|
||||
setupNewNameServerGroupHA: false
|
||||
};
|
||||
|
||||
const data = createReducer<NameServerGroup[], ActionTypes>(initialState.data as NameServerGroup[])
|
||||
.handleAction(actions.getNameServerGroups.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getNameServerGroups.failure, () => []);
|
||||
|
||||
const nameserverGroup = createReducer<NameServerGroup, ActionTypes>(initialState.nameserverGroup as NameServerGroup)
|
||||
.handleAction(actions.setNameServerGroup, (store, action) => action.payload);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getNameServerGroups.request, () => true)
|
||||
.handleAction(actions.getNameServerGroups.success, () => false)
|
||||
.handleAction(actions.getNameServerGroups.failure, () => false);
|
||||
|
||||
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getNameServerGroups.request, () => null)
|
||||
.handleAction(actions.getNameServerGroups.success, () => null)
|
||||
.handleAction(actions.getNameServerGroups.failure, (store, action) => action.payload);
|
||||
|
||||
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
|
||||
.handleAction(actions.getNameServerGroups.request, () => true)
|
||||
.handleAction(actions.getNameServerGroups.success, () => false)
|
||||
.handleAction(actions.getNameServerGroups.failure, () => false);
|
||||
|
||||
const deletedNameServerGroup = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deleteNameServerGroup)
|
||||
.handleAction(actions.deleteNameServerGroup.request, () => initialState.deleteNameServerGroup)
|
||||
.handleAction(actions.deleteNameServerGroup.success, (store, action) => action.payload)
|
||||
.handleAction(actions.deleteNameServerGroup.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setDeletedNameServerGroup, (store, action) => action.payload)
|
||||
.handleAction(actions.resetDeletedNameServerGroup, () => initialState.deleteNameServerGroup)
|
||||
|
||||
const savedNameServerGroup = createReducer<CreateResponse<NameServerGroup | null>, ActionTypes>(initialState.savedNameServerGroup)
|
||||
.handleAction(actions.saveNameServerGroup.request, () => initialState.savedNameServerGroup)
|
||||
.handleAction(actions.saveNameServerGroup.success, (store, action) => action.payload)
|
||||
.handleAction(actions.saveNameServerGroup.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedNameServerGroup, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedNameServerGroup, () => initialState.savedNameServerGroup)
|
||||
|
||||
const setupNewNameServerGroupVisible = createReducer<boolean, ActionTypes>(initialState.setupNewNameServerGroupVisible)
|
||||
.handleAction(actions.setSetupNewNameServerGroupVisible, (store, action) => action.payload)
|
||||
|
||||
const setupNewNameServerGroupHA = createReducer<boolean, ActionTypes>(initialState.setupNewNameServerGroupHA)
|
||||
.handleAction(actions.setSetupNewNameServerGroupHA, (store, action) => action.payload)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
nameserverGroup,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedNameServerGroup,
|
||||
savedNameServerGroup,
|
||||
setupNewNameServerGroupVisible,
|
||||
setupNewNameServerGroupHA
|
||||
});
|
||||
156
src/store/nameservers/sagas.ts
Normal file
156
src/store/nameservers/sagas.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
|
||||
import {NameServerGroup} from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
import serviceGroup from "../group/service";
|
||||
import {Group} from "../group/types";
|
||||
import {actions as groupActions} from "../group";
|
||||
|
||||
export function* getNameServerGroups(action: ReturnType<typeof actions.getNameServerGroups.request>): Generator {
|
||||
try {
|
||||
|
||||
yield put(actions.setDeletedNameServerGroup({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>))
|
||||
|
||||
const effect = yield call(service.getNameServerGroups, action.payload);
|
||||
const response = effect as ApiResponse<NameServerGroup[]>;
|
||||
|
||||
yield put(actions.getNameServerGroups.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getNameServerGroups.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setCreatedNameServerGroup(action: ReturnType<typeof actions.setSavedNameServerGroup>): Generator {
|
||||
yield put(actions.setSavedNameServerGroup(action.payload))
|
||||
}
|
||||
|
||||
export function* saveNameServerGroup(action: ReturnType<typeof actions.saveNameServerGroup.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setSavedNameServerGroup({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as CreateResponse<NameServerGroup | null>))
|
||||
|
||||
const nameserverGroupToSave = action.payload.payload
|
||||
|
||||
let groupsToCreate = nameserverGroupToSave.groupsToCreate
|
||||
if (!groupsToCreate) {
|
||||
groupsToCreate = []
|
||||
}
|
||||
|
||||
// first, create groups that were newly added by user
|
||||
const responsesGroup = yield all(groupsToCreate.map(g => call(serviceGroup.createGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {name: g}
|
||||
})
|
||||
))
|
||||
|
||||
const resGroups = (responsesGroup as ApiResponse<Group>[]).filter(r => r.statusCode === 200).map(g => (g.body as Group)).map(g => g.id)
|
||||
const newGroups = [...nameserverGroupToSave.groups, ...resGroups]
|
||||
|
||||
const payloadToSave = {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {
|
||||
id: nameserverGroupToSave.id,
|
||||
name: nameserverGroupToSave.name,
|
||||
description: nameserverGroupToSave.description,
|
||||
primary: nameserverGroupToSave.primary,
|
||||
domains: nameserverGroupToSave.domains,
|
||||
nameservers: nameserverGroupToSave.nameservers,
|
||||
groups: newGroups,
|
||||
enabled: nameserverGroupToSave.enabled,
|
||||
} as NameServerGroup
|
||||
}
|
||||
|
||||
let effect
|
||||
if (!nameserverGroupToSave.id) {
|
||||
effect = yield call(service.createNameServerGroup, payloadToSave);
|
||||
} else {
|
||||
payloadToSave.payload.id = nameserverGroupToSave.id
|
||||
effect = yield call(service.editNameServerGroup, payloadToSave);
|
||||
}
|
||||
|
||||
const response = effect as ApiResponse<NameServerGroup>;
|
||||
|
||||
yield put(actions.saveNameServerGroup.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as CreateResponse<NameServerGroup | null>));
|
||||
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
|
||||
yield put(actions.getNameServerGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
|
||||
} catch (err) {
|
||||
yield put(actions.saveNameServerGroup.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as CreateResponse<NameServerGroup | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setDeleteNameServerGroup(action: ReturnType<typeof actions.setDeletedNameServerGroup>): Generator {
|
||||
yield put(actions.setDeletedNameServerGroup(action.payload))
|
||||
}
|
||||
|
||||
export function* deleteNameServerGroup(action: ReturnType<typeof actions.deleteNameServerGroup.request>): Generator {
|
||||
try {
|
||||
yield call(actions.setDeletedNameServerGroup,{
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>)
|
||||
|
||||
const effect = yield call(service.deletedNameServerGroup, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
yield put(actions.deleteNameServerGroup.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
const nameserverGroup = (yield select(state => state.nameserverGroup.data)) as NameServerGroup[]
|
||||
yield put(actions.getNameServerGroups.success(nameserverGroup.filter((p:NameServerGroup) => p.id !== action.payload.payload)))
|
||||
} catch (err) {
|
||||
yield put(actions.deleteNameServerGroup.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.getNameServerGroups.request, getNameServerGroups),
|
||||
takeLatest(actions.saveNameServerGroup.request, saveNameServerGroup),
|
||||
takeLatest(actions.deleteNameServerGroup.request, deleteNameServerGroup)
|
||||
]);
|
||||
}
|
||||
|
||||
32
src/store/nameservers/service.ts
Normal file
32
src/store/nameservers/service.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { NameServerGroup } from './types';
|
||||
|
||||
export default {
|
||||
async getNameServerGroups(payload:RequestPayload<null>): Promise<ApiResponse<NameServerGroup[]>> {
|
||||
return apiClient.get<NameServerGroup[]>(
|
||||
`/api/dns/nameservers`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletedNameServerGroup(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/dns/nameservers/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createNameServerGroup(payload:RequestPayload<NameServerGroup>): Promise<ApiResponse<NameServerGroup>> {
|
||||
return apiClient.post<NameServerGroup>(
|
||||
`/api/dns/nameservers`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editNameServerGroup(payload:RequestPayload<NameServerGroup>): Promise<ApiResponse<NameServerGroup>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<NameServerGroup>(
|
||||
`/api/dns/nameservers/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
21
src/store/nameservers/types.ts
Normal file
21
src/store/nameservers/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface NameServerGroup {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
primary: boolean
|
||||
domains: string[]
|
||||
nameservers: NameServer[]
|
||||
groups: string[]
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface NameServer {
|
||||
ip: string
|
||||
ns_type: string
|
||||
port: number
|
||||
}
|
||||
|
||||
export interface NameServerGroupToSave extends NameServerGroup
|
||||
{
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { actions as UserActions } from './user';
|
||||
import { actions as GroupActions } from './group';
|
||||
import { actions as RuleActions } from './rule';
|
||||
import { actions as RouteActions } from './route';
|
||||
import { actions as NameServerGroupActions } from './nameservers';
|
||||
|
||||
export default {
|
||||
peer: PeerActions,
|
||||
@@ -11,5 +12,6 @@ export default {
|
||||
user: UserActions,
|
||||
group: GroupActions,
|
||||
rule: RuleActions,
|
||||
route: RouteActions
|
||||
route: RouteActions,
|
||||
nameserverGroup: NameServerGroupActions
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { reducer as user } from './user';
|
||||
import { reducer as group } from './group';
|
||||
import { reducer as rule } from './rule';
|
||||
import { reducer as route } from './route';
|
||||
import { reducer as nameserverGroup } from './nameservers';
|
||||
|
||||
export default combineReducers({
|
||||
peer,
|
||||
@@ -13,5 +14,6 @@ export default combineReducers({
|
||||
user,
|
||||
group,
|
||||
rule,
|
||||
route
|
||||
route,
|
||||
nameserverGroup
|
||||
});
|
||||
|
||||
136
src/utils/groups.tsx
Normal file
136
src/utils/groups.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Col, Divider, Row, Tag} from "antd";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
|
||||
export const useGetGroupTagHelpers = () => {
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [groupTagFilterAll, setGroupTagFilterAll] = useState(false)
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const {value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color="blue"
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = []
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v)
|
||||
}
|
||||
})
|
||||
setSelectedTagGroups(validatedValues)
|
||||
};
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<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>
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = ''
|
||||
const g = groups.find(_g => _g.name === label)
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const getExistingAndToCreateGroupsLists = (groupNameList: string[]): [string[], string[]] => {
|
||||
const groupIDList = groups?.filter(g => groupNameList.includes(g.name)).map(g => g.id || '') || []
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const existingGroupsNames: string[] = groups?.map(g => g.name);
|
||||
const groupNameListToCreate = groupNameList.filter(s => !existingGroupsNames.includes(s))
|
||||
return [groupIDList, groupNameListToCreate]
|
||||
}
|
||||
|
||||
const getGroupNamesFromIDs = (groupIDList: string[]): string[] => {
|
||||
if (!groupIDList) {
|
||||
return []
|
||||
}
|
||||
|
||||
return groups?.filter(g => groupIDList.includes(g.id!)).map(g => g.name || '') || []
|
||||
}
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
if (!value.length) {
|
||||
return Promise.reject(new Error("Please enter at least one group"))
|
||||
}
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(new Error("Group names with just spaces are not allowed"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (groupTagFilterAll) {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
} else {
|
||||
setTagGroups(groups?.map(g => g.name) || [])
|
||||
}
|
||||
}, [groups])
|
||||
|
||||
return {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
selectedTagGroups,
|
||||
setGroupTagFilterAll,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,7 @@ export const AccessControl = () => {
|
||||
{ruleToAction &&
|
||||
<>
|
||||
<Title level={5}>Delete rule "{ruleToAction ? ruleToAction.name : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to delete peer from your account?</Paragraph>
|
||||
<Paragraph>Are you sure you want to delete this rule from your account?</Paragraph>
|
||||
</>
|
||||
}
|
||||
</Space>,
|
||||
@@ -342,8 +342,8 @@ export const AccessControl = () => {
|
||||
})
|
||||
return (
|
||||
<Popover
|
||||
onVisibleChange={onPopoverVisibleChange}
|
||||
visible={groupPopupVisible}
|
||||
onOpenChange={onPopoverVisibleChange}
|
||||
open={groupPopupVisible}
|
||||
content={content}
|
||||
title={null}>
|
||||
<Button type="link" onClick={() => setRuleAndView(rule)}>{label}</Button>
|
||||
|
||||
445
src/views/DNS.tsx
Normal file
445
src/views/DNS.tsx
Normal file
@@ -0,0 +1,445 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as nsGroupActions} from '../store/nameservers';
|
||||
import {Container} from "../components/Container";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown,
|
||||
Input,
|
||||
Menu, message, Modal,
|
||||
Popover, Radio, RadioChangeEvent,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {filter} from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {Group} from "../store/group/types";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {NameServerGroup, NameServer} from "../store/nameservers/types";
|
||||
import NameServerGroupUpdate from "../components/NameServerGroupUpdate";
|
||||
import {ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
|
||||
const {Title, Paragraph} = Typography;
|
||||
const {Column} = Table;
|
||||
const {confirm} = Modal;
|
||||
|
||||
interface NameserverGroupDataTable extends NameServerGroup {
|
||||
key: string
|
||||
}
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
export const DNS = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {
|
||||
getGroupNamesFromIDs,
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const nsGroup = useSelector((state: RootState) => state.nameserverGroup.data);
|
||||
const failed = useSelector((state: RootState) => state.nameserverGroup.failed);
|
||||
const loading = useSelector((state: RootState) => state.nameserverGroup.loading);
|
||||
const updateNameServerGroupVisible = useSelector((state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible)
|
||||
const savedNSGroup = useSelector((state: RootState) => state.nameserverGroup.savedNameServerGroup)
|
||||
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState(false as boolean | undefined)
|
||||
const [nsGroupToAction, setNsGroupToAction] = useState(null as NameserverGroupDataTable | null);
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionAllEnable, setOptionAllEnable] = useState('enabled');
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [dataTable, setDataTable] = useState([] as NameserverGroupDataTable[]);
|
||||
const pageSizeOptions = [
|
||||
{label: "5", value: "5"},
|
||||
{label: "10", value: "10"},
|
||||
{label: "15", value: "15"}
|
||||
]
|
||||
|
||||
const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'}, {label: 'All', value: 'all'}]
|
||||
|
||||
// setUserAndView makes the UserUpdate drawer visible (right side) and sets the user object
|
||||
const setUserAndView = (nsGroup: NameServerGroup) => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
|
||||
dispatch(nsGroupActions.setNameServerGroup({
|
||||
id: nsGroup.id,
|
||||
name: nsGroup.name,
|
||||
primary: nsGroup.primary,
|
||||
domains: nsGroup.domains,
|
||||
description: nsGroup.description,
|
||||
nameservers: nsGroup.nameservers,
|
||||
groups: nsGroup.groups,
|
||||
enabled: nsGroup.enabled,
|
||||
} as NameServerGroup));
|
||||
}
|
||||
|
||||
const transformDataTable = (d: NameServerGroup[]): NameserverGroupDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as NameserverGroupDataTable))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(nsGroupActions.getNameServerGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [nsGroup])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch, optionAllEnable])
|
||||
|
||||
const filterDataTable = (): NameServerGroup[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f = filter(nsGroup, (f: NameServerGroup) =>
|
||||
((f.name ).toLowerCase().includes(t) ||
|
||||
f.name.includes(t) || t === "" ||
|
||||
getGroupNamesFromIDs(f.groups).find(u => u.toLowerCase().trim().includes(t) ||
|
||||
f.domains.find(d => d.toLowerCase().trim().includes(t)) ||
|
||||
f.nameservers.find(n => n.ip.includes(t))))
|
||||
) as NameServerGroup[]
|
||||
if (optionAllEnable !== "all") {
|
||||
f = filter(f, (f) => f.enabled)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => {
|
||||
setOptionAllEnable(value)
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
|
||||
const onClickEdit = () => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
|
||||
dispatch(nsGroupActions.setNameServerGroup({
|
||||
id: nsGroupToAction?.id,
|
||||
name: nsGroupToAction?.name,
|
||||
primary: nsGroupToAction?.primary,
|
||||
domains: nsGroupToAction?.domains,
|
||||
description: nsGroupToAction?.description,
|
||||
groups: nsGroupToAction?.groups,
|
||||
enabled: nsGroupToAction?.enabled,
|
||||
nameservers: nsGroupToAction?.nameservers,
|
||||
} as NameServerGroup));
|
||||
}
|
||||
|
||||
const showConfirmDelete = () => {
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
{nsGroupToAction &&
|
||||
<>
|
||||
<Title level={5}>Delete Nameserver group "{nsGroupToAction ? nsGroupToAction.name : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to delete this nameserver group from your account?</Paragraph>
|
||||
</>
|
||||
}
|
||||
</Space>,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(nsGroupActions.deleteNameServerGroup.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: nsGroupToAction?.id || ''
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
setNsGroupToAction(null);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, rowGroups: string[] | null, userToAction: NameserverGroupDataTable) => {
|
||||
|
||||
let groupsMap = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
groupsMap.set(g.id!, g)
|
||||
})
|
||||
|
||||
let displayGroups: Group[] = []
|
||||
if (rowGroups) {
|
||||
displayGroups = rowGroups.filter(g => groupsMap.get(g)).map(g => groupsMap.get(g)!)
|
||||
}
|
||||
|
||||
let btn = <Button type="link" onClick={() => setUserAndView(userToAction)}>{displayGroups.length}</Button>
|
||||
if (!displayGroups || displayGroups!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const content = displayGroups?.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>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={onPopoverVisibleChange}
|
||||
open={groupPopupVisible}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const renderPopoverDomains = (_: string, inputDomains: string[] | null, userToAction: NameserverGroupDataTable) => {
|
||||
var domains = [] as string[]
|
||||
if (inputDomains?.length) {
|
||||
domains = inputDomains
|
||||
}
|
||||
|
||||
let btn = <Button type="link" onClick={() => setUserAndView(userToAction)}>{domains.length ? domains.length : 0}</Button>
|
||||
if (!domains || domains!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const content = domains?.map((d, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{d}</strong>
|
||||
</Tag>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={onPopoverVisibleChange}
|
||||
open={groupPopupVisible}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateNameServerGroupVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
}
|
||||
}, [updateNameServerGroupVisible])
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedNSGroup.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedNSGroup.success) {
|
||||
message.success({
|
||||
content: 'User has been successfully saved.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(false));
|
||||
dispatch(nsGroupActions.setSavedNameServerGroup({...savedNSGroup, success: false}));
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null))
|
||||
} else if (savedNSGroup.error) {
|
||||
message.error({
|
||||
content: 'Failed to update user. You might not have enough permissions.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(nsGroupActions.setSavedNameServerGroup({...savedNSGroup, error: null}));
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null))
|
||||
}
|
||||
}, [savedNSGroup])
|
||||
|
||||
const onPopoverVisibleChange = () => {
|
||||
if (updateNameServerGroupVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
} else {
|
||||
setGroupPopupVisible(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
key: "edit",
|
||||
label: (<Button type="text" onClick={() => onClickEdit()}>View</Button>)
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: (<Button type="text" onClick={() => showConfirmDelete()}>Delete</Button>)
|
||||
},
|
||||
]
|
||||
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
|
||||
const onClickAddNewNSGroup = () => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
|
||||
dispatch(nsGroupActions.setNameServerGroup({
|
||||
enabled: true,
|
||||
} as NameServerGroup))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Nameservers</Title>
|
||||
<Paragraph>Add nameservers for domain name resolution in your NetBird network</Paragraph>
|
||||
<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}/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
<Radio.Group
|
||||
options={optionsAllEnabled}
|
||||
onChange={onChangeAllEnabled}
|
||||
value={optionAllEnable}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
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}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button type="primary" onClick={onClickAddNewNSGroup}>Add Nameserver</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.code} description={failed.message} type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} users`)
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Name" dataIndex="name" align="center"
|
||||
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) => {
|
||||
return <Button type="text"
|
||||
onClick={() => setUserAndView(record as NameserverGroupDataTable)}
|
||||
className="tooltip-label">{(text && text.trim() !== "") ? text : (record as NameServerGroup).id}</Button>
|
||||
}}
|
||||
/>
|
||||
<Column title="Status" dataIndex="enabled" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="green">enabled</Tag> :
|
||||
<Tag color="red">disabled</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Nameservers" dataIndex="nameservers" align="center"
|
||||
render={(nameservers:NameServer[]) => (
|
||||
<>
|
||||
{nameservers.map(nameserver => (
|
||||
<Tag key={nameserver.ip}>
|
||||
{nameserver.ip}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Column title="All domains" dataIndex="primary" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="blue">yes</Tag> :
|
||||
<Tag>no</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Match domains" dataIndex="domains" align="center"
|
||||
render={(text, record: NameserverGroupDataTable) => {
|
||||
return renderPopoverDomains(text, record.domains, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="Groups" dataIndex="groupsCount" align="center"
|
||||
render={(text, record: NameserverGroupDataTable) => {
|
||||
return renderPopoverGroups(text, record.groups, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="" align="center" width="30px"
|
||||
render={(text, record) => {
|
||||
return (
|
||||
<Dropdown.Button type="text" overlay={actionsMenu}
|
||||
trigger={["click"]}
|
||||
onOpenChange={visible => {
|
||||
if (visible) setNsGroupToAction(record as NameserverGroupDataTable)
|
||||
}}></Dropdown.Button>)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<NameServerGroupUpdate/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DNS;
|
||||
Reference in New Issue
Block a user