Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e57e5b726d | ||
|
|
2c4ada0ad8 | ||
|
|
8195587c85 | ||
|
|
adf6e1e71f | ||
|
|
b733a186ae | ||
|
|
5d901470c2 | ||
|
|
29ab28847d | ||
|
|
0361825e04 | ||
|
|
2fa33ec06a | ||
|
|
c677eeaae4 |
37
README.md
37
README.md
@@ -36,24 +36,43 @@ Disclaimer. We believe that proper user management system is not a trivial task
|
||||
use Auth0 service that covers all our needs (user management, social login, JTW for the management API).
|
||||
Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
|
||||
|
||||
1. install [Docker](https://docs.docker.com/get-docker/)
|
||||
2. register [Auth0](https://auth0.com/) account
|
||||
3. running Wiretrustee UI Dashboard requires the following Auth0 environmental variables to be set (see docker command below):
|
||||
1. Install [Docker](https://docs.docker.com/get-docker/)
|
||||
2. Register [Auth0](https://auth0.com/) account
|
||||
3. Running Wiretrustee UI Dashboard requires the following Auth0 environmental variables to be set (see docker command below):
|
||||
|
||||
```AUTH0_DOMAIN``` ```AUTH0_CLIENT_ID``` ```AUTH0_AUDIENCE```
|
||||
`AUTH0_DOMAIN` `AUTH0_CLIENT_ID` `AUTH0_AUDIENCE`
|
||||
|
||||
To obtain these, please use [Auth0 React SDK Guide](https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0) up until "Configure Allowed Web Origins"
|
||||
|
||||
4. Wiretrustee UI Dashboard uses Wiretrustee Management Service HTTP API, so setting ```WIRETRUSTEE_MGMT_API_ENDPOINT``` is required. Most likely it will be ```http://localhost:33071``` if you are hosting Management API on the same server.
|
||||
4. Wiretrustee UI Dashboard uses Wiretrustee Management Service HTTP API, so setting `NETBIRD_MGMT_API_ENDPOINT` is required. Most likely it will be `http://localhost:33071` if you are hosting Management API on the same server.
|
||||
5. Run docker container without SSL (Let's Encrypt):
|
||||
|
||||
```docker run -d --name wiretrustee-dashboard --rm -p 80:80 -p 443:443 -e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> -e AUTH0_CLIENT_ID=<SET YOUR CLIENT ID> -e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> -e WIRETRUSTEE_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> wiretrustee/dashboard:main```
|
||||
```shell
|
||||
docker run -d --name wiretrustee-dashboard \
|
||||
--rm -p 80:80 -p 443:443 \
|
||||
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
|
||||
-e AUTH0_CLIENT_ID=<SET YOUR CLIENT ID> \
|
||||
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
|
||||
wiretrustee/dashboard:main
|
||||
```
|
||||
6. Run docker container with SSL (Let's Encrypt):
|
||||
|
||||
```docker run -d --name wiretrustee-dashboard --rm -p 80:80 -p 443:443 -e NGINX_SSL_PORT=443 -e LETSENCRYPT_DOMAIN=<YOUR PUBLIC DOMAIN> -e LETSENCRYPT_EMAIL=<YOUR EMAIL> -e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> -e AUTH0_CLIENT_ID=<SET YOUR CLEITN ID> -e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> -e WIRETRUSTEE_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> wiretrustee/dashboard:main```
|
||||
```shell
|
||||
docker run -d --name wiretrustee-dashboard \
|
||||
--rm -p 80:80 -p 443:443 \
|
||||
-e NGINX_SSL_PORT=443 \
|
||||
-e LETSENCRYPT_DOMAIN=<YOUR PUBLIC DOMAIN> \
|
||||
-e LETSENCRYPT_EMAIL=<YOUR EMAIL> \
|
||||
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
|
||||
-e AUTH0_CLIENT_ID=<SET YOUR CLEITN ID> \
|
||||
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
|
||||
wiretrustee/dashboard:main
|
||||
```
|
||||
|
||||
## How to run local development
|
||||
1. Install node 16
|
||||
2. create and update the src/.local-config.json file. This file should contain values to be replaced from src/config.json
|
||||
2. create and update the `src/.local-config.json` file. This file should contain values to be replaced from `src/config.json`
|
||||
3. run `npm install`
|
||||
4. run `npm run start dev`
|
||||
4. run `npm run start dev`
|
||||
|
||||
@@ -20,6 +20,7 @@ import {useGetAccessTokenSilently} from "./utils/token";
|
||||
import {User} from "./store/user/types";
|
||||
import {SecureLoading} from "./components/Loading";
|
||||
import DNS from "./views/DNS";
|
||||
import Activity from "./views/Activity";
|
||||
|
||||
|
||||
|
||||
@@ -105,6 +106,7 @@ function App() {
|
||||
<Route path="/routes" component={withOidcSecure(Routes)}/>
|
||||
<Route path="/users" component={withOidcSecure(Users)}/>
|
||||
<Route path="/dns" component={withOidcSecure(DNS)}/>
|
||||
<Route path="/activity" component={withOidcSecure(Activity)}/>
|
||||
</Switch>
|
||||
</Content>
|
||||
<FooterComponent/>
|
||||
|
||||
@@ -5,7 +5,7 @@ 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';
|
||||
import {useOidc, useOidcUser} from '@axa-fr/react-oidc';
|
||||
import {useOidc, useOidcIdToken, useOidcUser} from '@axa-fr/react-oidc';
|
||||
import {getConfig} from "../config";
|
||||
import {User} from "../store/user/types";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
@@ -23,6 +23,7 @@ const Navbar = () => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {oidcUser} = useOidcUser();
|
||||
const {idTokenPayload} = useOidcIdToken()
|
||||
const user = oidcUser;
|
||||
const [currentUser, setCurrentUser] = useState({} as User)
|
||||
|
||||
@@ -39,13 +40,14 @@ const Navbar = () => {
|
||||
{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'}
|
||||
{label: (<Link to="/users">Users</Link>), key: '/users'},
|
||||
{label: (<Link to="/activity">Activity</Link>), key: '/activity'}
|
||||
] as ItemType[]
|
||||
|
||||
const userEmailKey = 'user-email'
|
||||
const userLogoutKey = 'user-logout'
|
||||
const userDividerKey = 'user-divider'
|
||||
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns"]
|
||||
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns", "/activity"]
|
||||
const [menuItems, setMenuItems] = useState(items)
|
||||
const logoutWithRedirect = () =>
|
||||
logout("/", {client_id: config.clientId});
|
||||
@@ -84,9 +86,13 @@ const Navbar = () => {
|
||||
if (users.length === 0 && isRefreshingUserState) {
|
||||
return
|
||||
}
|
||||
let runUser = oidcUser
|
||||
if (!oidcUser) {
|
||||
runUser = idTokenPayload
|
||||
}
|
||||
setIsRefreshingUserState(false)
|
||||
if (oidcUser && oidcUser.sub) {
|
||||
const found = users.find(u => u.id == oidcUser.sub)
|
||||
if (runUser) {
|
||||
const found = users.find(u => u.is_current ? u.is_current : runUser.sub ? u.id == runUser.sub : false)
|
||||
if (found) {
|
||||
setCurrentUser(found)
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ const PeerUpdate = () => {
|
||||
max={59}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Possible domain name after saving"
|
||||
label="New peer domain name preview"
|
||||
tooltip="If the domain name already exists, we add an increment number suffix to it"
|
||||
style={{margin: '1px'}}
|
||||
>
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
Typography
|
||||
} from "antd";
|
||||
import {CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import {Route} from "../store/route/types";
|
||||
import {Route, RouteToSave} from "../store/route/types";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import cidrRegex from 'cidr-regex';
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
transformGroupedDataTable
|
||||
} from '../utils/routes'
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
|
||||
@@ -38,6 +39,17 @@ interface FormRoute extends Route {
|
||||
}
|
||||
|
||||
const RouteUpdate = () => {
|
||||
const {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator
|
||||
} = useGetGroupTagHelpers()
|
||||
const {Option} = Select;
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible)
|
||||
@@ -46,7 +58,6 @@ const RouteUpdate = () => {
|
||||
const route = useSelector((state: RootState) => state.route.route)
|
||||
const routes = useSelector((state: RootState) => state.route.data)
|
||||
const savedRoute = useSelector((state: RootState) => state.route.savedRoute)
|
||||
// const [groupedDataTable, setGroupedDataTable] = useState([] as GroupedDataTable[]);
|
||||
const [previousRouteKey, setPreviousRouteKey] = useState("")
|
||||
const [editName, setEditName] = useState(false)
|
||||
const [editDescription, setEditDescription] = useState(false)
|
||||
@@ -62,13 +73,14 @@ const RouteUpdate = () => {
|
||||
const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG)
|
||||
const defaultStatusMSG = "Status"
|
||||
const [statusMSG, setStatusMSG] = useState(defaultStatusMSG)
|
||||
const [peerNameToIP, peerIPToName] = initPeerMaps(peers);
|
||||
const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers);
|
||||
const [newRoute, setNewRoute] = useState(false)
|
||||
|
||||
const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]
|
||||
|
||||
useEffect(() => {
|
||||
if (setupNewRouteHA) {
|
||||
setRoutingPeerMSG("Add additional routing peer")
|
||||
if (!newRoute ) {
|
||||
setRoutingPeerMSG(defaultRoutingPeerMSG)
|
||||
setMasqueradeMSG("Update Masquerade")
|
||||
setStatusMSG("Update Status")
|
||||
} else {
|
||||
@@ -77,7 +89,7 @@ const RouteUpdate = () => {
|
||||
setStatusMSG(defaultStatusMSG)
|
||||
setPreviousRouteKey("")
|
||||
}
|
||||
}, [setupNewRouteHA])
|
||||
}, [newRoute])
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
@@ -96,9 +108,15 @@ const RouteUpdate = () => {
|
||||
|
||||
const fRoute = {
|
||||
...route,
|
||||
groups: getGroupNamesFromIDs(route.groups)
|
||||
} as FormRoute
|
||||
setFormRoute(fRoute)
|
||||
setPreviousRouteKey(fRoute.network_id + fRoute.network)
|
||||
if (!route.network_id) {
|
||||
setNewRoute(true)
|
||||
} else {
|
||||
setNewRoute(false)
|
||||
}
|
||||
form.setFieldsValue(fRoute)
|
||||
}, [route])
|
||||
|
||||
@@ -114,15 +132,17 @@ const RouteUpdate = () => {
|
||||
}
|
||||
})
|
||||
|
||||
const createRouteToSave = (inputRoute: FormRoute): Route => {
|
||||
const createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
|
||||
let peerIDList = inputRoute.peer.split(routePeerSeparator)
|
||||
let peerID: string
|
||||
if (peerIDList[1]) {
|
||||
peerID = peerIDList[1]
|
||||
peerID = peerIPToID[peerIDList[1]]
|
||||
} else {
|
||||
peerID = peerNameToIP[inputRoute.peer]
|
||||
peerID = peerIPToID[peerNameToIP[inputRoute.peer]]
|
||||
}
|
||||
|
||||
let [ existingGroups, groupsToCreate ] = getExistingAndToCreateGroupsLists(inputRoute.groups)
|
||||
|
||||
return {
|
||||
id: inputRoute.id,
|
||||
network: inputRoute.network,
|
||||
@@ -131,8 +151,10 @@ const RouteUpdate = () => {
|
||||
peer: peerID,
|
||||
enabled: inputRoute.enabled,
|
||||
masquerade: inputRoute.masquerade,
|
||||
metric: inputRoute.metric
|
||||
} as Route
|
||||
metric: inputRoute.metric,
|
||||
groups: existingGroups,
|
||||
groupsToCreate: groupsToCreate,
|
||||
} as RouteToSave
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
@@ -145,7 +167,7 @@ const RouteUpdate = () => {
|
||||
payload: routeToSave
|
||||
}))
|
||||
} else {
|
||||
let groupedDataTable = transformGroupedDataTable(routes, peerIPToName)
|
||||
let groupedDataTable = transformGroupedDataTable(routes, peers)
|
||||
groupedDataTable.forEach((group) => {
|
||||
if (group.key == previousRouteKey) {
|
||||
group.groupedRoutes.forEach((route) => {
|
||||
@@ -195,13 +217,14 @@ const RouteUpdate = () => {
|
||||
setVisibleNewRoute(false)
|
||||
setSetupNewRouteHA(false)
|
||||
setPreviousRouteKey("")
|
||||
setNewRoute(false)
|
||||
}
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormRoute({...formRoute, ...data})
|
||||
}
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
const peerDropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
</>
|
||||
@@ -227,13 +250,32 @@ const RouteUpdate = () => {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const peerValidator = (_: RuleObject, value: string) => {
|
||||
|
||||
if (value == "" && newRoute) {
|
||||
return Promise.reject(new Error("Please select routing one peer"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const selectPreValidator = (obj: RuleObject, value: string[]) => {
|
||||
if (setupNewRouteHA && formRoute.peer == '') {
|
||||
let [, newGroups ] = getExistingAndToCreateGroupsLists(value)
|
||||
if (newGroups.length > 0) {
|
||||
return Promise.reject(new Error("You can't add new Groups from the group update view, please remove:\"" + newGroups +"\""))
|
||||
}
|
||||
}
|
||||
return selectValidator(obj, value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{route &&
|
||||
<Drawer
|
||||
headerStyle={{display: "none"}}
|
||||
forceRender={true}
|
||||
visible={setupNewRouteVisible}
|
||||
open={setupNewRouteVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
autoFocus={true}
|
||||
@@ -241,11 +283,11 @@ const RouteUpdate = () => {
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedRoute.loading}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedRoute.loading}
|
||||
onClick={handleFormSubmit}>{`${formRoute.network_id ? 'Save' : 'Create'}`}</Button>
|
||||
onClick={handleFormSubmit}>{`${newRoute ? 'Create' : 'Save'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
|
||||
<Form layout="vertical" form={form} requiredMark={false} onValuesChange={onChange}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
@@ -278,7 +320,7 @@ const RouteUpdate = () => {
|
||||
}]}
|
||||
>
|
||||
<Input placeholder="e.g. aws-eu-central-1-vpc" ref={inputNameRef}
|
||||
disabled={!setupNewRouteHA}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)} autoComplete="off"
|
||||
maxLength={40}/>
|
||||
@@ -294,7 +336,7 @@ const RouteUpdate = () => {
|
||||
style={{marginTop: 24}}
|
||||
>
|
||||
<Input placeholder="Add description..." ref={inputDescriptionRef}
|
||||
disabled={!setupNewRouteHA}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off" maxLength={200}/>
|
||||
@@ -321,7 +363,7 @@ const RouteUpdate = () => {
|
||||
tooltip="Use CIDR notation. e.g. 192.168.10.0/24 or 172.16.0.0/16"
|
||||
rules={[{validator: networkRangeValidator}]}
|
||||
>
|
||||
<Input placeholder="e.g. 172.16.0.0/16" disabled={!setupNewRouteHA}
|
||||
<Input placeholder="e.g. 172.16.0.0/16" disabled={!setupNewRouteHA && !newRoute}
|
||||
autoComplete="off" minLength={9} maxLength={43}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -343,12 +385,13 @@ const RouteUpdate = () => {
|
||||
name="peer"
|
||||
label={routingPeerMSG}
|
||||
tooltip="Assign a peer as a routing peer for the Network CIDR"
|
||||
rules={[{validator:peerValidator}]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
style={{width: '100%'}}
|
||||
placeholder="Select Peer"
|
||||
dropdownRender={dropDownRender}
|
||||
dropdownRender={peerDropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
/>
|
||||
@@ -360,7 +403,7 @@ const RouteUpdate = () => {
|
||||
label={masqueradeMSG}
|
||||
tooltip={masqueradeDisabledMSG}
|
||||
>
|
||||
<Switch size={"small"} disabled={!setupNewRouteHA} checked={formRoute.masquerade}/>
|
||||
<Switch size={"small"} disabled={!setupNewRouteHA && !newRoute} checked={formRoute.masquerade}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
@@ -372,6 +415,28 @@ const RouteUpdate = () => {
|
||||
<InputNumber min={1} max={9999} autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label="Distribution groups"
|
||||
tooltip="NetBird will advertise this route to peers that belong to the following groups"
|
||||
rules={[{validator: selectPreValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the network route"
|
||||
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">
|
||||
|
||||
@@ -233,7 +233,8 @@ const UserUpdate = () => {
|
||||
role: "",
|
||||
status: "",
|
||||
auto_groups: [],
|
||||
name: user.name
|
||||
name: user.name,
|
||||
is_current: user.is_current,
|
||||
} as User));
|
||||
setFormUser({} as FormUser)
|
||||
toggleEditName(false)
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, {useState} from 'react';
|
||||
import { Button } from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
import { formatNetBirdUP } from "./common"
|
||||
|
||||
export const LinuxTab = () => {
|
||||
|
||||
@@ -30,9 +31,7 @@ export const LinuxTab = () => {
|
||||
{
|
||||
key: 3,
|
||||
title: 'Run NetBird and log in the browser:',
|
||||
commands: [
|
||||
`sudo netbird up`
|
||||
].join('\n'),
|
||||
commands: formatNetBirdUP(),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
|
||||
@@ -5,21 +5,12 @@ import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
import { getConfig } from "../../config";
|
||||
import Paragraph from 'antd/lib/skeleton/Paragraph';
|
||||
const { grpcApiOrigin } = getConfig();
|
||||
import { formatNetBirdUP } from "./common"
|
||||
|
||||
|
||||
|
||||
export const UbuntuTab = () => {
|
||||
|
||||
const formatNetBirdUP = () => {
|
||||
let cmd = "sudo netbird up"
|
||||
if (grpcApiOrigin) {
|
||||
cmd = "sudo netbird up --management-url " + grpcApiOrigin
|
||||
}
|
||||
return [
|
||||
cmd
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
|
||||
13
src/components/addpeer/common.ts
Normal file
13
src/components/addpeer/common.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {getConfig} from "../../config";
|
||||
const { grpcApiOrigin } = getConfig();
|
||||
|
||||
|
||||
export const formatNetBirdUP = () => {
|
||||
let cmd = "sudo netbird up"
|
||||
if (grpcApiOrigin) {
|
||||
cmd = "sudo netbird up --management-url " + grpcApiOrigin
|
||||
}
|
||||
return [
|
||||
cmd
|
||||
].join('\n')
|
||||
}
|
||||
26
src/store/dns-settings/actions.ts
Normal file
26
src/store/dns-settings/actions.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
|
||||
import {DNSSettings, DNSSettingsToSave} from './types';
|
||||
import {ApiError, CreateResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getDNSSettings: createAsyncAction(
|
||||
'GET_DNSSettings_REQUEST',
|
||||
'GET_DNSSettings_SUCCESS',
|
||||
'GET_DNSSettings_FAILURE',
|
||||
)<RequestPayload<null>, DNSSettings, ApiError>(),
|
||||
|
||||
saveDNSSettings: createAsyncAction(
|
||||
'SAVE_DNSSettings_REQUEST',
|
||||
'SAVE_DNSSettings_SUCCESS',
|
||||
'SAVE_DNSSettings_FAILURE',
|
||||
)<RequestPayload<DNSSettingsToSave>, CreateResponse<DNSSettings | null>, CreateResponse<DNSSettings | null>>(),
|
||||
setSavedDNSSettings: createAction('SET_CREATE_DNSSettings')<CreateResponse<DNSSettings | null>>(),
|
||||
resetSavedDNSSettings: createAction('RESET_CREATE_DNSSettings')<null>(),
|
||||
|
||||
setDNSSettings: createAction('SET_DNSSettings')<DNSSettings>(),
|
||||
setSetupNewDNSSettingsVisible: createAction('SET_SETUP_NEW_DNSSettings_VISIBLE')<boolean>(),
|
||||
setSetupNewDNSSettingsHA: createAction('SET_SETUP_NEW_DNSSettings_HA')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
export default actions;
|
||||
7
src/store/dns-settings/index.ts
Normal file
7
src/store/dns-settings/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 };
|
||||
79
src/store/dns-settings/reducer.ts
Normal file
79
src/store/dns-settings/reducer.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { DNSSettings } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, CreateResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: DNSSettings | null;
|
||||
dnsSettings: DNSSettings | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
savedDNSSettings: CreateResponse<DNSSettings | null>;
|
||||
setupNewDNSSettingsVisible: boolean;
|
||||
setupNewDNSSettingsHA: boolean
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: null,
|
||||
dnsSettings: null,
|
||||
loading: false,
|
||||
failed: null,
|
||||
saving: false,
|
||||
savedDNSSettings: <CreateResponse<DNSSettings | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
setupNewDNSSettingsVisible: false,
|
||||
setupNewDNSSettingsHA: false
|
||||
};
|
||||
|
||||
const data = createReducer<DNSSettings, ActionTypes>(initialState.data as DNSSettings)
|
||||
.handleAction(actions.getDNSSettings.success,(settings, action) => action.payload)
|
||||
.handleAction(actions.getDNSSettings.failure,(settings, _) => settings);
|
||||
|
||||
const dnsSettings = createReducer<DNSSettings, ActionTypes>(initialState.dnsSettings as DNSSettings)
|
||||
.handleAction(actions.setDNSSettings, (store, action) => action.payload);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getDNSSettings.request, () => true)
|
||||
.handleAction(actions.getDNSSettings.success, () => false)
|
||||
.handleAction(actions.getDNSSettings.failure, () => false);
|
||||
|
||||
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getDNSSettings.request, () => null)
|
||||
.handleAction(actions.getDNSSettings.success, () => null)
|
||||
.handleAction(actions.getDNSSettings.failure, (store, action) => action.payload);
|
||||
|
||||
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
|
||||
.handleAction(actions.getDNSSettings.request, () => true)
|
||||
.handleAction(actions.getDNSSettings.success, () => false)
|
||||
.handleAction(actions.getDNSSettings.failure, () => false);
|
||||
|
||||
const savedDNSSettings = createReducer<CreateResponse<DNSSettings | null>, ActionTypes>(initialState.savedDNSSettings)
|
||||
.handleAction(actions.saveDNSSettings.request, () => initialState.savedDNSSettings)
|
||||
.handleAction(actions.saveDNSSettings.success, (store, action) => action.payload)
|
||||
.handleAction(actions.saveDNSSettings.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedDNSSettings, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedDNSSettings, () => initialState.savedDNSSettings)
|
||||
|
||||
const setupNewDNSSettingsVisible = createReducer<boolean, ActionTypes>(initialState.setupNewDNSSettingsVisible)
|
||||
.handleAction(actions.setSetupNewDNSSettingsVisible, (store, action) => action.payload)
|
||||
|
||||
const setupNewDNSSettingsHA = createReducer<boolean, ActionTypes>(initialState.setupNewDNSSettingsHA)
|
||||
.handleAction(actions.setSetupNewDNSSettingsHA, (store, action) => action.payload)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
dnsSettings,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
savedDNSSettings,
|
||||
setupNewDNSSettingsVisible,
|
||||
setupNewDNSSettingsHA
|
||||
});
|
||||
95
src/store/dns-settings/sagas.ts
Normal file
95
src/store/dns-settings/sagas.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {all, call, put, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, CreateResponse} from '../../services/api-client/types';
|
||||
import {DNSSettings} 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* getDNSSettings(action: ReturnType<typeof actions.getDNSSettings.request>): Generator {
|
||||
try {
|
||||
|
||||
const effect = yield call(service.getDNSSettings, action.payload);
|
||||
const response = effect as ApiResponse<DNSSettings>;
|
||||
|
||||
yield put(actions.getDNSSettings.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getDNSSettings.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* saveDNSSettings(action: ReturnType<typeof actions.saveDNSSettings.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setSavedDNSSettings({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as CreateResponse<DNSSettings | null>))
|
||||
|
||||
const settingsToSave = action.payload.payload
|
||||
|
||||
let groupsToCreate = settingsToSave.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 = [...settingsToSave.disabled_management_groups, ...resGroups]
|
||||
|
||||
const payloadToSave = {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {
|
||||
disabled_management_groups: newGroups,
|
||||
} as DNSSettings
|
||||
}
|
||||
|
||||
let effect = yield call(service.editDNSSettings, payloadToSave);
|
||||
const response = effect as ApiResponse<DNSSettings>;
|
||||
|
||||
yield put(actions.saveDNSSettings.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as CreateResponse<DNSSettings | null>));
|
||||
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
|
||||
yield put(actions.getDNSSettings.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
|
||||
} catch (err) {
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
yield put(actions.saveDNSSettings.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as CreateResponse<DNSSettings | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getDNSSettings.request, getDNSSettings),
|
||||
takeLatest(actions.saveDNSSettings.request, saveDNSSettings),
|
||||
]);
|
||||
}
|
||||
|
||||
18
src/store/dns-settings/service.ts
Normal file
18
src/store/dns-settings/service.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { DNSSettings } from './types';
|
||||
|
||||
export default {
|
||||
async getDNSSettings(payload:RequestPayload<null>): Promise<ApiResponse<DNSSettings>> {
|
||||
return apiClient.get<DNSSettings>(
|
||||
`/api/dns/settings`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editDNSSettings(payload:RequestPayload<DNSSettings>): Promise<ApiResponse<DNSSettings>> {
|
||||
return apiClient.put<DNSSettings>(
|
||||
`/api/dns/settings`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
8
src/store/dns-settings/types.ts
Normal file
8
src/store/dns-settings/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface DNSSettings {
|
||||
disabled_management_groups: string[]
|
||||
}
|
||||
|
||||
export interface DNSSettingsToSave extends DNSSettings
|
||||
{
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
14
src/store/event/actions.ts
Normal file
14
src/store/event/actions.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {ActionType, createAsyncAction} from 'typesafe-actions';
|
||||
import {Event} from './types';
|
||||
import {ApiError, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getEvents: createAsyncAction(
|
||||
'GET_EVENTS_REQUEST',
|
||||
'GET_EVENTS_SUCCESS',
|
||||
'GET_EVENTS_FAILURE',
|
||||
)<RequestPayload<null>, Event[], ApiError>(),
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
export default actions;
|
||||
7
src/store/event/index.ts
Normal file
7
src/store/event/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 };
|
||||
38
src/store/event/reducer.ts
Normal file
38
src/store/event/reducer.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { Event } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, CreateResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: Event[] | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
loading: false,
|
||||
failed: null,
|
||||
};
|
||||
|
||||
const data = createReducer<Event[], ActionTypes>(initialState.data as Event[])
|
||||
.handleAction(actions.getEvents.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getEvents.failure, () => []);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getEvents.request, () => true)
|
||||
.handleAction(actions.getEvents.success, () => false)
|
||||
.handleAction(actions.getEvents.failure, () => false);
|
||||
|
||||
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getEvents.request, () => null)
|
||||
.handleAction(actions.getEvents.success, () => null)
|
||||
.handleAction(actions.getEvents.failure, (store, action) => action.payload);
|
||||
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
loading,
|
||||
failed,
|
||||
});
|
||||
23
src/store/event/sagas.ts
Normal file
23
src/store/event/sagas.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {all, call, put, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse} from '../../services/api-client/types';
|
||||
import {Event} from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
|
||||
export function* getEvents(action: ReturnType<typeof actions.getEvents.request>): Generator {
|
||||
try {
|
||||
const effect = yield call(service.getEvents, action.payload);
|
||||
const response = effect as ApiResponse<Event[]>;
|
||||
|
||||
yield put(actions.getEvents.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getEvents.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getEvents.request, getEvents),
|
||||
]);
|
||||
}
|
||||
|
||||
12
src/store/event/service.ts
Normal file
12
src/store/event/service.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import {Event} from './types';
|
||||
|
||||
export default {
|
||||
async getEvents(payload:RequestPayload<null>): Promise<ApiResponse<Event[]>> {
|
||||
return apiClient.get<Event[]>(
|
||||
`/api/events`,
|
||||
payload
|
||||
);
|
||||
}
|
||||
};
|
||||
9
src/store/event/types.ts
Normal file
9
src/store/event/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface Event {
|
||||
id: string;
|
||||
timestamp: string
|
||||
activity: string
|
||||
activity_code: string
|
||||
initiator_id: string
|
||||
target_id: string
|
||||
meta: { [key: string]: string }
|
||||
}
|
||||
@@ -9,6 +9,8 @@ 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 { sagas as eventSagas } from './event';
|
||||
import { sagas as dnsSettingsSagas } from './dns-settings';
|
||||
|
||||
import rootReducer from './root-reducer';
|
||||
import { apiClient } from '../services/api-client';
|
||||
@@ -27,5 +29,7 @@ sagaMiddleware.run(ruleSagas);
|
||||
sagaMiddleware.run(groupSagas);
|
||||
sagaMiddleware.run(routeSagas);
|
||||
sagaMiddleware.run(nameserverGroupSagas);
|
||||
sagaMiddleware.run(eventSagas);
|
||||
sagaMiddleware.run(dnsSettingsSagas);
|
||||
|
||||
export { apiClient, rootReducer, store };
|
||||
@@ -59,7 +59,7 @@ export function* deletePeer(action: ReturnType<typeof actions.deletedPeer.reques
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
const peers = (yield select(state => state.peer.data)) as Peer[]
|
||||
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.ip !== action.payload.payload)))
|
||||
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.id !== action.payload.payload)))
|
||||
} catch (err) {
|
||||
yield put(actions.deletedPeer.failure({
|
||||
loading: false,
|
||||
|
||||
@@ -40,6 +40,10 @@ export interface PeerIPToName {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerIPToID {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerDataTable extends Peer {
|
||||
key: string;
|
||||
groups: Group[];
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { actions as PeerActions } from './peer';
|
||||
import { actions as SetupKeyActions } from './setup-key';
|
||||
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';
|
||||
import {actions as PeerActions} from './peer';
|
||||
import {actions as SetupKeyActions} from './setup-key';
|
||||
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';
|
||||
import {actions as EventActions} from './event';
|
||||
import {actions as DNSSettingsActions} from './dns-settings';
|
||||
|
||||
export default {
|
||||
peer: PeerActions,
|
||||
@@ -13,5 +15,7 @@ export default {
|
||||
group: GroupActions,
|
||||
rule: RuleActions,
|
||||
route: RouteActions,
|
||||
nameserverGroup: NameServerGroupActions
|
||||
nameserverGroup: NameServerGroupActions,
|
||||
event: EventActions,
|
||||
dnsSettings: DNSSettingsActions
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ import { reducer as group } from './group';
|
||||
import { reducer as rule } from './rule';
|
||||
import { reducer as route } from './route';
|
||||
import { reducer as nameserverGroup } from './nameservers';
|
||||
import { reducer as event } from './event';
|
||||
import { reducer as dnsSettings } from './dns-settings';
|
||||
|
||||
export default combineReducers({
|
||||
peer,
|
||||
@@ -15,5 +17,7 @@ export default combineReducers({
|
||||
group,
|
||||
rule,
|
||||
route,
|
||||
nameserverGroup
|
||||
nameserverGroup,
|
||||
event,
|
||||
dnsSettings
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
|
||||
import {Route} from './types';
|
||||
import {Route, RouteToSave} from './types';
|
||||
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
@@ -13,7 +13,7 @@ const actions = {
|
||||
'SAVE_ROUTE_REQUEST',
|
||||
'SAVE_ROUTE_SUCCESS',
|
||||
'SAVE_ROUTE_FAILURE',
|
||||
)<RequestPayload<Route>, CreateResponse<Route | null>, CreateResponse<Route | null>>(),
|
||||
)<RequestPayload<RouteToSave>, CreateResponse<Route | null>, CreateResponse<Route | null>>(),
|
||||
setSavedRoute: createAction('SET_CREATE_ROUTE')<CreateResponse<Route | null>>(),
|
||||
resetSavedRoute: createAction('RESET_CREATE_ROUTE')<null>(),
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../servi
|
||||
import {Route} 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* getRoutes(action: ReturnType<typeof actions.getRoutes.request>): Generator {
|
||||
try {
|
||||
@@ -40,6 +43,21 @@ export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>)
|
||||
|
||||
const routeToSave = action.payload.payload
|
||||
|
||||
let groupsToCreate = routeToSave.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 = [...routeToSave.groups, ...resGroups]
|
||||
|
||||
const payloadToSave = {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {
|
||||
@@ -50,7 +68,8 @@ export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>)
|
||||
metric: routeToSave.metric,
|
||||
network: routeToSave.network,
|
||||
network_id: routeToSave.network_id,
|
||||
peer: routeToSave.peer
|
||||
peer: routeToSave.peer,
|
||||
groups: newGroups
|
||||
} as Route
|
||||
}
|
||||
|
||||
@@ -72,8 +91,19 @@ export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>)
|
||||
data: response.body
|
||||
} as CreateResponse<Route | null>));
|
||||
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
|
||||
yield put(actions.getRoutes.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
|
||||
} catch (err) {
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
|
||||
yield put(actions.saveRoute.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
export interface Route {
|
||||
id?: string
|
||||
description: string
|
||||
@@ -8,4 +9,10 @@ export interface Route {
|
||||
network_type?: string
|
||||
metric?: number
|
||||
masquerade: boolean
|
||||
groups: string[]
|
||||
}
|
||||
|
||||
export interface RouteToSave extends Route
|
||||
{
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
@@ -3,15 +3,15 @@ export interface User {
|
||||
email?: string;
|
||||
name: string;
|
||||
role: string;
|
||||
status: string
|
||||
auto_groups: string[]
|
||||
status: string;
|
||||
auto_groups: string[];
|
||||
is_current?: boolean;
|
||||
}
|
||||
|
||||
export interface FormUser extends User {
|
||||
autoGroupsNames: string[]
|
||||
}
|
||||
|
||||
export interface UserToSave extends User
|
||||
{
|
||||
export interface UserToSave extends User {
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,20 @@ export const formatDate = date => {
|
||||
return new Date(date).toLocaleDateString("en-GB", { weekday: 'short', year: '2-digit', month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
export const capitalize = text => {
|
||||
if (!text) {
|
||||
return text
|
||||
}
|
||||
return text.charAt(0).toUpperCase() + text.slice(1)
|
||||
}
|
||||
|
||||
export const formatDateTime = date => {
|
||||
if (new Date(date).getTime() > new Date("2099-12-31").getTime()) {
|
||||
return new Date(date).toLocaleDateString("en-GB", { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' });
|
||||
}
|
||||
return new Date(date).toLocaleDateString("en-GB", { weekday: 'short', year: '2-digit', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' });
|
||||
}
|
||||
|
||||
export const classNames = (...classes) => {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
@@ -94,12 +94,16 @@ export const useGetGroupTagHelpers = () => {
|
||||
return groups?.filter(g => groupIDList.includes(g.id!)).map(g => g.name || '') || []
|
||||
}
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
const selectValidator = (obj: RuleObject, value: string[]) => {
|
||||
if (!value.length) {
|
||||
return Promise.reject(new Error("Please enter at least one group"))
|
||||
}
|
||||
|
||||
return selectValidatorEmptyStrings(obj,value)
|
||||
}
|
||||
|
||||
const selectValidatorEmptyStrings = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
@@ -131,6 +135,7 @@ export const useGetGroupTagHelpers = () => {
|
||||
setGroupTagFilterAll,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator
|
||||
selectValidator,
|
||||
selectValidatorEmptyStrings
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Peer, PeerNameToIP, PeerIPToName} from "../store/peer/types";
|
||||
import {Peer, PeerIPToID, PeerIPToName, PeerNameToIP} from "../store/peer/types";
|
||||
import {Route} from "../store/route/types";
|
||||
|
||||
export const routePeerSeparator = " - "
|
||||
@@ -7,22 +7,26 @@ export const masqueradeDisabledMSG = "Enabling this option hides other NetBird n
|
||||
|
||||
export const masqueradeEnabledMSG = "Disabling this option stops hiding all traffic coming from other NetBird peers behind the routing peer local address when accessing the target Network CIDR. You will need to configure routes for your NetBird network pointing to your routing peer on your local routers or other devices."
|
||||
|
||||
export const peerToPeerIP = (name:string,ip:string):string => {
|
||||
export const peerToPeerIP = (name: string, ip: string): string => {
|
||||
return name + routePeerSeparator + ip
|
||||
}
|
||||
|
||||
export const initPeerMaps = (peers:Peer[]): [PeerNameToIP, PeerIPToName] => {
|
||||
export const initPeerMaps = (peers: Peer[]): [PeerNameToIP, PeerIPToName, PeerIPToID] => {
|
||||
let peerNameToIP = {} as PeerNameToIP
|
||||
let peerIPToName = {} as PeerIPToName
|
||||
peers.forEach((p) =>{
|
||||
let peerIPToID = {} as PeerIPToID
|
||||
peers.forEach((p) => {
|
||||
peerNameToIP[p.name] = p.ip
|
||||
peerIPToName[p.ip] = p.name
|
||||
peerIPToID[p.ip] = p.id ? p.id : ""
|
||||
})
|
||||
return [ peerNameToIP, peerIPToName]
|
||||
return [peerNameToIP, peerIPToName, peerIPToID]
|
||||
}
|
||||
|
||||
export interface RouteDataTable extends Route {
|
||||
key: string;
|
||||
peer_ip: string;
|
||||
peer_name: string;
|
||||
}
|
||||
|
||||
export interface GroupedDataTable {
|
||||
@@ -34,38 +38,47 @@ export interface GroupedDataTable {
|
||||
description: string
|
||||
routesCount: number
|
||||
groupedRoutes: RouteDataTable[]
|
||||
routesGroups: string[]
|
||||
}
|
||||
|
||||
export const transformDataTable = (d:Route[],peerIPToName:PeerIPToName):RouteDataTable[] => {
|
||||
return d.map(p => {
|
||||
export const transformDataTable = (routes: Route[], peers: Peer[]): RouteDataTable[] => {
|
||||
|
||||
let peerMap = Object.fromEntries(peers.map(p => [p.id, p]));
|
||||
return routes.map(route => {
|
||||
return {
|
||||
key: p.id,
|
||||
...p,
|
||||
peer: peerIPToName[p.peer] ? peerIPToName[p.peer] : p.peer,
|
||||
key: route.id,
|
||||
...route,
|
||||
peer: route.peer,
|
||||
peer_ip: peerMap[route.peer] ? peerMap[route.peer].ip : route.peer,
|
||||
peer_name: peerMap[route.peer] ? peerMap[route.peer].name : route.peer,
|
||||
} as RouteDataTable
|
||||
})
|
||||
}
|
||||
|
||||
export const transformGroupedDataTable = (routes:Route[],peerIPToName:PeerIPToName):GroupedDataTable[] => {
|
||||
export const transformGroupedDataTable = (routes: Route[], peers: Peer[]): GroupedDataTable[] => {
|
||||
let keySet = new Set(routes.map(r => {
|
||||
return r.network_id + r.network
|
||||
}))
|
||||
|
||||
let groupedRoutes:GroupedDataTable[] = []
|
||||
let groupedRoutes: GroupedDataTable[] = []
|
||||
|
||||
keySet.forEach((p) => {
|
||||
let hasEnabled = false
|
||||
let lastRoute:Route
|
||||
let listedRoutes:Route[] = []
|
||||
let lastRoute: Route
|
||||
let listedRoutes: Route[] = []
|
||||
let groupList: string[] = []
|
||||
routes.forEach((r) => {
|
||||
if ( p === r.network_id + r.network ) {
|
||||
if (p === r.network_id + r.network) {
|
||||
lastRoute = r
|
||||
if (r.enabled) {
|
||||
hasEnabled = true
|
||||
}
|
||||
listedRoutes.push(r)
|
||||
groupList = groupList.concat(r.groups)
|
||||
}
|
||||
})
|
||||
let groupDataTableRoutes = transformDataTable(listedRoutes,peerIPToName)
|
||||
groupList = groupList.filter((value, index, arrary) => arrary.indexOf(value) === index)
|
||||
let groupDataTableRoutes = transformDataTable(listedRoutes, peers)
|
||||
groupedRoutes.push({
|
||||
key: p.toString(),
|
||||
network_id: lastRoute!.network_id,
|
||||
@@ -75,6 +88,7 @@ export const transformGroupedDataTable = (routes:Route[],peerIPToName:PeerIPToNa
|
||||
enabled: hasEnabled,
|
||||
routesCount: groupDataTableRoutes.length,
|
||||
groupedRoutes: groupDataTableRoutes,
|
||||
routesGroups: groupList,
|
||||
})
|
||||
})
|
||||
return groupedRoutes
|
||||
|
||||
272
src/views/Activity.tsx
Normal file
272
src/views/Activity.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as eventActions} from '../store/event';
|
||||
import {Container} from "../components/Container";
|
||||
import {Alert, Card, Col, Input, Row, Select, Space, Table, Typography,} from "antd";
|
||||
import {Event} from "../store/event/types";
|
||||
import {filter} from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import UserUpdate from "../components/UserUpdate";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {capitalize, formatDateTime} from "../utils/common";
|
||||
import {User} from "../store/user/types";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
|
||||
interface EventDataTable extends Event {
|
||||
}
|
||||
|
||||
export const Activity = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {oidcUser} = useOidcUser();
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const events = useSelector((state: RootState) => state.event.data);
|
||||
const failed = useSelector((state: RootState) => state.event.failed);
|
||||
const loading = useSelector((state: RootState) => state.event.loading);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const setupKeys = useSelector((state: RootState) => state.setupKey.data);
|
||||
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [dataTable, setDataTable] = useState([] as EventDataTable[]);
|
||||
const pageSizeOptions = [
|
||||
{label: "5", value: "5"},
|
||||
{label: "10", value: "10"},
|
||||
{label: "15", value: "15"},
|
||||
{label: "20", value: "20"}
|
||||
]
|
||||
|
||||
const transformDataTable = (d: Event[]): EventDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as EventDataTable))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(eventActions.getEvents.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(events))
|
||||
}, [events])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch])
|
||||
|
||||
const filterDataTable = (): Event[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let usrsMatch: User[] = filter(users, (u: User) => (u.name)?.toLowerCase().includes(t) || (u.email)?.toLowerCase().includes(t)) as User[]
|
||||
let f: Event[] = filter(events, (f: Event) =>
|
||||
((f.activity || f.id).toLowerCase().includes(t) || t === "" || usrsMatch.find(u => u.id === f.initiator_id))
|
||||
) as Event[]
|
||||
return f
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable()
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
|
||||
const getActivityRow = (group:string,text:string) => {
|
||||
return <Row> <Text>Group <Text type="secondary">{group}</Text> {text}</Text> </Row>
|
||||
}
|
||||
|
||||
const renderActivity = (event: EventDataTable) => {
|
||||
let body = <Text>{event.activity}</Text>
|
||||
switch (event.activity_code) {
|
||||
case "peer.group.add":
|
||||
return getActivityRow(event.meta.group,"added to peer")
|
||||
case "peer.group.delete":
|
||||
return getActivityRow(event.meta.group,"removed from peer")
|
||||
case "user.group.add":
|
||||
return getActivityRow(event.meta.group,"added to user")
|
||||
case "user.group.delete":
|
||||
return getActivityRow(event.meta.group,"removed from user")
|
||||
case "setupkey.group.add":
|
||||
return getActivityRow(event.meta.group,"added to setup key")
|
||||
case "setupkey.group.delete":
|
||||
return getActivityRow(event.meta.group,"removed setup key")
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
return getActivityRow(event.meta.group,"added to disabled management DNS setting")
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
return getActivityRow(event.meta.group,"removed from disabled management DNS setting")
|
||||
}
|
||||
return body
|
||||
}
|
||||
const renderInitiator = (event: EventDataTable) => {
|
||||
let body = <></>
|
||||
const user = users?.find(u => u.id === event.initiator_id)
|
||||
switch (event.activity_code) {
|
||||
case "setupkey.peer.add":
|
||||
const key = setupKeys?.find(k => k.id === event.initiator_id)
|
||||
if (key) {
|
||||
body = <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{key.name}</Text> </Row>
|
||||
<Row> <Text type="secondary">Setup Key</Text> </Row>
|
||||
</span>
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (user) {
|
||||
body = <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{user.name ? user.name : user.id}</Text> </Row>
|
||||
<Row> <Text type="secondary">{user.email ? user.email : "User"}</Text> </Row>
|
||||
</span>
|
||||
return body
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
const renderMultiRowSpan = (primaryRowText:string,secondaryRowText:string) => {
|
||||
return <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{primaryRowText}</Text> </Row>
|
||||
<Row> <Text type="secondary">{secondaryRowText}</Text> </Row>
|
||||
</span>
|
||||
}
|
||||
|
||||
const renderTarget = (event: EventDataTable) => {
|
||||
if (event.activity_code === "account.create" || event.activity_code === "user.join") {
|
||||
return "-"
|
||||
}
|
||||
const user = users?.find(u => u.id === event.target_id)
|
||||
switch (event.activity_code) {
|
||||
case "account.create":
|
||||
case "user.join":
|
||||
return "-"
|
||||
case "rule.add":
|
||||
case "rule.delete":
|
||||
case "rule.update":
|
||||
return renderMultiRowSpan(event.meta.name,"Rule")
|
||||
case "setupkey.add":
|
||||
case "setupkey.revoke":
|
||||
case "setupkey.update":
|
||||
case "setupkey.overuse":
|
||||
let cType:string
|
||||
cType = capitalize(event.meta.type)
|
||||
return renderMultiRowSpan(event.meta.name,cType+" setup key "+event.meta.key)
|
||||
case "group.add":
|
||||
case "group.update":
|
||||
return renderMultiRowSpan(event.meta.name,"Group")
|
||||
case "nameserver.group.add":
|
||||
case "nameserver.group.update":
|
||||
case "nameserver.group.delete":
|
||||
return renderMultiRowSpan(event.meta.name,"Nameserver group")
|
||||
case "setupkey.peer.add":
|
||||
case "user.peer.add":
|
||||
case "user.peer.delete":
|
||||
case "peer.ssh.enable":
|
||||
case "peer.ssh.disable":
|
||||
case "peer.rename":
|
||||
return renderMultiRowSpan(event.meta.fqdn,event.meta.ip)
|
||||
case "route.add":
|
||||
case "route.delete":
|
||||
case "route.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Route for range " + event.meta.network_range)
|
||||
case "user.group.add":
|
||||
case "user.group.delete":
|
||||
if (user) {
|
||||
return renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
|
||||
}
|
||||
return "n/a"
|
||||
case "setupkey.group.add":
|
||||
case "setupkey.group.delete":
|
||||
return renderMultiRowSpan(event.meta.setupkey,"Setup Key")
|
||||
case "peer.group.add":
|
||||
case "peer.group.delete":
|
||||
return renderMultiRowSpan(event.meta.peer_fqdn,event.meta.peer_ip)
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
return renderMultiRowSpan("","System setting")
|
||||
case "user.invite":
|
||||
if (user) {
|
||||
return renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
|
||||
}
|
||||
}
|
||||
|
||||
return event.target_id
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Activity</Title>
|
||||
<Paragraph>Here you can see all the account and network activity events</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">
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.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}
|
||||
size="small"
|
||||
>
|
||||
<Column title="Timestamp" dataIndex="timestamp"
|
||||
render={(text, record, index) => {
|
||||
return formatDateTime(text)
|
||||
}}
|
||||
/>
|
||||
<Column title="Activity" dataIndex="activity"
|
||||
render={(text, record, index) => {
|
||||
return renderActivity(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Initiated By" dataIndex="initiator_id"
|
||||
render={(text, record, index) => {
|
||||
return renderInitiator(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Target" dataIndex="target_id"
|
||||
render={(text, record, index) => {
|
||||
return renderTarget(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<UserUpdate/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Activity;
|
||||
@@ -1,350 +1,58 @@
|
||||
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,
|
||||
Tabs,
|
||||
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 {NameServer, NameServerGroup} from "../store/nameservers/types";
|
||||
import type { TabsProps } from 'antd';
|
||||
import NameServerGroupUpdate from "../components/NameServerGroupUpdate";
|
||||
import {ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import Nameservers from "./Nameservers";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import DNSSettingsForm from "./DNSSettings";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as dnsSettingsActions} from '../store/dns-settings';
|
||||
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 [showTutorial, setShowTutorial] = useState(false)
|
||||
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))
|
||||
}
|
||||
const dnsSettingsData = useSelector((state: RootState) => state.dnsSettings.data)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(nsGroupActions.getNameServerGroups.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (nsGroup.length > 0) {
|
||||
setShowTutorial(false)
|
||||
} else {
|
||||
setShowTutorial(true)
|
||||
}
|
||||
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) {
|
||||
let errorMsg = "Failed to update nameserver group"
|
||||
switch (savedNSGroup.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update user. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = savedNSGroup.error.data.message ? savedNSGroup.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(nsGroupActions.setSavedNameServerGroup({...savedNSGroup, error: null}));
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null))
|
||||
}
|
||||
}, [savedNSGroup])
|
||||
|
||||
const onPopoverVisibleChange = () => {
|
||||
if (updateNameServerGroupVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
} else {
|
||||
setGroupPopupVisible(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const itemsMenuAction = [
|
||||
const nsTabKey = '1'
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: "edit",
|
||||
label: (<Button type="text" onClick={() => onClickEdit()}>View</Button>)
|
||||
key: nsTabKey,
|
||||
label: 'Nameservers',
|
||||
children: <Nameservers/>,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: (<Button type="text" onClick={() => showConfirmDelete()}>Delete</Button>)
|
||||
key: '2',
|
||||
label: 'Settings',
|
||||
children: <DNSSettingsForm/>,
|
||||
},
|
||||
]
|
||||
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
|
||||
const onClickAddNewNSGroup = () => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
|
||||
dispatch(nsGroupActions.setNameServerGroup({
|
||||
enabled: true,
|
||||
primary: true,
|
||||
} as NameServerGroup))
|
||||
const onTabClick = (key:string) => {
|
||||
if (key == nsTabKey) {
|
||||
if (!dnsSettingsData) return
|
||||
dispatch(dnsSettingsActions.setDNSSettings({
|
||||
disabled_management_groups: getGroupNamesFromIDs(dnsSettingsData.disabled_management_groups),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -352,127 +60,13 @@ export const DNS = () => {
|
||||
<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>
|
||||
{!showTutorial &&
|
||||
<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"
|
||||
className={`access-control-table ${showTutorial ? "card-table card-table-no-placeholder" : "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>
|
||||
{showTutorial &&
|
||||
<Space direction="vertical" size="small" align="center"
|
||||
style={{display: 'flex', padding: '45px 15px', justifyContent: 'center'}}>
|
||||
<Paragraph type="secondary"
|
||||
style={{textAlign: "center", whiteSpace: "pre-line"}}>
|
||||
It looks like you don't have any nameservers. {"\n"}
|
||||
Get started by adding one to your network!
|
||||
</Paragraph>
|
||||
<Button type="primary" onClick={onClickAddNewNSGroup}>Add
|
||||
Nameserver</Button>
|
||||
</Space>
|
||||
}
|
||||
</Card>
|
||||
</Space>
|
||||
<Tabs
|
||||
defaultActiveKey={nsTabKey}
|
||||
items={items}
|
||||
onTabClick={onTabClick}
|
||||
animated={{ inkBar: true, tabPane: false }}
|
||||
tabPosition="top"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
179
src/views/DNSSettings.tsx
Normal file
179
src/views/DNSSettings.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
message,
|
||||
Select,
|
||||
Space,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import {actions as dnsSettingsActions} from '../store/dns-settings';
|
||||
import {DNSSettings, DNSSettingsToSave} from "../store/dns-settings/types";
|
||||
import {actions as nsGroupActions} from "../store/nameservers";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
export const DNSSettingsForm = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidatorEmptyStrings
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const dnsSettings = useSelector((state: RootState) => state.dnsSettings.dnsSettings)
|
||||
const dnsSettingsData = useSelector((state: RootState) => state.dnsSettings.data)
|
||||
const savedDNSSettings = useSelector((state: RootState) => state.dnsSettings.savedDNSSettings)
|
||||
const loading = useSelector((state: RootState) => state.dnsSettings.loading);
|
||||
|
||||
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(dnsSettingsActions.getDNSSettings.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dnsSettingsData) return
|
||||
dispatch(dnsSettingsActions.setDNSSettings({
|
||||
disabled_management_groups: getGroupNamesFromIDs(dnsSettingsData.disabled_management_groups),
|
||||
}))
|
||||
}, [dnsSettingsData])
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(dnsSettings)
|
||||
}, [dnsSettings])
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedDNSSettings.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedDNSSettings.success) {
|
||||
message.success({
|
||||
content: 'DNS settings has been successfully saved.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(dnsSettingsActions.setSavedDNSSettings({...savedDNSSettings, success: false}));
|
||||
dispatch(dnsSettingsActions.resetSavedDNSSettings(null))
|
||||
} else if (savedDNSSettings.error) {
|
||||
let errorMsg = "Failed to update DNS settings"
|
||||
switch (savedDNSSettings.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update DNS settings. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = savedDNSSettings.error.data.message ? savedDNSSettings.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(dnsSettingsActions.setSavedDNSSettings({...savedDNSSettings, error: null}));
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null))
|
||||
}
|
||||
}, [savedDNSSettings])
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let dnsSettingsToSave = createDNSSettingsToSave(values)
|
||||
dispatch(dnsSettingsActions.saveDNSSettings.request({
|
||||
getAccessTokenSilently:getAccessTokenSilently,
|
||||
payload: dnsSettingsToSave
|
||||
}))
|
||||
})
|
||||
.then(() => {
|
||||
console.log("issued the request")
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
let msg = "please check the fields and try again"
|
||||
if (errorInfo.errorFields) {
|
||||
msg = errorInfo.errorFields[0].errors[0]
|
||||
}
|
||||
message.error({
|
||||
content: msg,
|
||||
duration: 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const createDNSSettingsToSave = (values: DNSSettings): DNSSettingsToSave => {
|
||||
let [existingGroups, newGroups] = getExistingAndToCreateGroupsLists(values.disabled_management_groups)
|
||||
return {
|
||||
disabled_management_groups: existingGroups,
|
||||
groupsToCreate: newGroups
|
||||
} as DNSSettingsToSave
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph>Manage your account's DNS settings</Paragraph>
|
||||
<Col>
|
||||
<Form
|
||||
name="basic"
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
onFinish={handleFormSubmit}
|
||||
>
|
||||
<Space direction={"vertical"}
|
||||
style={{ display: 'flex' }}>
|
||||
<Card
|
||||
title="DNS Management"
|
||||
loading={loading}
|
||||
>
|
||||
<Form.Item
|
||||
label="Disable DNS management for these groups"
|
||||
name="disabled_management_groups"
|
||||
tooltip="Peers in these groups will have their DNS management disabled and require manual configuration for domain name resolution"
|
||||
rules={[{validator: selectValidatorEmptyStrings}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Select.Option key={m}>{optionRender(m)}</Select.Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
<Form.Item style={{ textAlign:'center' }} >
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DNSSettingsForm;
|
||||
476
src/views/Nameservers.tsx
Normal file
476
src/views/Nameservers.tsx
Normal file
@@ -0,0 +1,476 @@
|
||||
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 {NameServer, NameServerGroup} 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 Nameservers = () => {
|
||||
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 [showTutorial, setShowTutorial] = useState(false)
|
||||
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(() => {
|
||||
if (nsGroup.length > 0) {
|
||||
setShowTutorial(false)
|
||||
} else {
|
||||
setShowTutorial(true)
|
||||
}
|
||||
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: 'Nameserver 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) {
|
||||
let errorMsg = "Failed to update nameserver group"
|
||||
switch (savedNSGroup.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update nameserver group. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = savedNSGroup.error.data.message ? savedNSGroup.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
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,
|
||||
primary: true,
|
||||
} as NameServerGroup))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
{!showTutorial &&
|
||||
<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"
|
||||
className={`access-control-table ${showTutorial ? "card-table card-table-no-placeholder" : "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>
|
||||
{showTutorial &&
|
||||
<Space direction="vertical" size="small" align="center"
|
||||
style={{display: 'flex', padding: '45px 15px', justifyContent: 'center'}}>
|
||||
<Paragraph type="secondary"
|
||||
style={{textAlign: "center", whiteSpace: "pre-line"}}>
|
||||
It looks like you don't have any nameservers. {"\n"}
|
||||
Get started by adding one to your network!
|
||||
</Paragraph>
|
||||
<Button type="primary" onClick={onClickAddNewNSGroup}>Add
|
||||
Nameserver</Button>
|
||||
</Space>
|
||||
}
|
||||
</Card>
|
||||
</Space>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Nameservers;
|
||||
@@ -237,7 +237,7 @@ export const Peers = () => {
|
||||
const showConfirmDelete = () => {
|
||||
let peerRoutes: string[] = []
|
||||
routes.forEach((r) => {
|
||||
if (r.peer == peerToAction?.ip) {
|
||||
if (r.peer == peerToAction?.id) {
|
||||
peerRoutes.push(r.network_id)
|
||||
}
|
||||
})
|
||||
@@ -288,7 +288,7 @@ export const Peers = () => {
|
||||
onOk() {
|
||||
dispatch(peerActions.deletedPeer.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: peerToAction ? peerToAction.ip : ''
|
||||
payload: (peerToAction && peerToAction.id) ? peerToAction.id! : ""
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
@@ -551,7 +551,10 @@ export const Peers = () => {
|
||||
|
||||
<Column title="LastSeen" dataIndex="last_seen"
|
||||
render={(text, record, index) => {
|
||||
return (record as PeerDataTable).connected ? 'just now' : timeAgo(text)
|
||||
let dt = new Date(text)
|
||||
return <Popover content={dt.toLocaleString()}>
|
||||
{(record as PeerDataTable).connected ? 'just now' : timeAgo(text)}
|
||||
</Popover>
|
||||
}}
|
||||
/>
|
||||
<Column title="OS" dataIndex="os"
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Input,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
Modal, Popover,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Row,
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import {Container} from "../components/Container";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {Route} from "../store/route/types";
|
||||
import {Route, RouteToSave} from "../store/route/types";
|
||||
import {actions as routeActions} from "../store/route";
|
||||
import {actions as peerActions} from "../store/peer";
|
||||
import {filter, sortBy} from "lodash";
|
||||
@@ -42,6 +42,10 @@ import {
|
||||
transformGroupedDataTable
|
||||
} from '../utils/routes'
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {Group} from "../store/group/types";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
@@ -50,7 +54,11 @@ const {confirm} = Modal;
|
||||
export const Routes = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const {
|
||||
getGroupNamesFromIDs,
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const routes = useSelector((state: RootState) => state.route.data);
|
||||
const failed = useSelector((state: RootState) => state.route.failed);
|
||||
const loading = useSelector((state: RootState) => state.route.loading);
|
||||
@@ -58,6 +66,7 @@ export const Routes = () => {
|
||||
const savedRoute = useSelector((state: RootState) => state.route.savedRoute);
|
||||
const peers = useSelector((state: RootState) => state.peer.data)
|
||||
const loadingPeer = useSelector((state: RootState) => state.peer.loading);
|
||||
const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible)
|
||||
const [showTutorial, setShowTutorial] = useState(true)
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionAllEnable, setOptionAllEnable] = useState('enabled');
|
||||
@@ -67,6 +76,7 @@ export const Routes = () => {
|
||||
const [routeToAction, setRouteToAction] = useState(null as RouteDataTable | null);
|
||||
const [groupedDataTable, setGroupedDataTable] = useState([] as GroupedDataTable[]);
|
||||
const [expandRowsOnClick, setExpandRowsOnClick] = useState(true)
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState(false as boolean | undefined)
|
||||
|
||||
const [peerNameToIP, peerIPToName] = initPeerMaps(peers);
|
||||
|
||||
@@ -83,10 +93,6 @@ export const Routes = () => {
|
||||
key: "view",
|
||||
label: (<Button type="text" block onClick={() => onClickViewRoute()}>View</Button>)
|
||||
},
|
||||
// {
|
||||
// key: "delete",
|
||||
// label: (<Button type="text" block onClick={() => showConfirmDeactivate()}>Deactivate</Button>)
|
||||
// },
|
||||
{
|
||||
key: "delete",
|
||||
label: (<Button type="text" block onClick={() => showConfirmDelete()}>Delete</Button>)
|
||||
@@ -104,12 +110,15 @@ export const Routes = () => {
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(peerActions.getPeers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
const filterGroupedDataTable = (routes: GroupedDataTable[]): GroupedDataTable[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f: GroupedDataTable[] = filter(routes, (f) =>
|
||||
(f.network_id.toLowerCase().includes(t) || f.network.toLowerCase().includes(t) || f.description.toLowerCase().includes(t) || t === "")
|
||||
(f.network_id.toLowerCase().includes(t) || f.network.toLowerCase().includes(t) ||
|
||||
f.description.toLowerCase().includes(t) || t === "" ||
|
||||
getGroupNamesFromIDs(f.routesGroups).find(u => u.toLowerCase().trim().includes(t)) )
|
||||
) as GroupedDataTable[]
|
||||
if (optionAllEnable !== "all") {
|
||||
f = filter(f, (f) => f.enabled)
|
||||
@@ -118,7 +127,7 @@ export const Routes = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peerIPToName)))
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
|
||||
}, [dataTable])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -126,12 +135,12 @@ export const Routes = () => {
|
||||
setShowTutorial(false)
|
||||
} else {
|
||||
setShowTutorial(isShowTutorial(routes))
|
||||
setDataTable(sortBy(transformDataTable(routes, peerIPToName), "network_id"))
|
||||
setDataTable(sortBy(transformDataTable(routes, peers), "network_id"))
|
||||
}
|
||||
}, [routes])
|
||||
|
||||
useEffect(() => {
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peerIPToName)))
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
|
||||
}, [textToSearch, optionAllEnable])
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
@@ -151,10 +160,19 @@ export const Routes = () => {
|
||||
dispatch(routeActions.setSavedRoute({...savedRoute, success: false}))
|
||||
dispatch(routeActions.resetSavedRoute(null))
|
||||
} else if (savedRoute.error) {
|
||||
let errorMsg = "Failed to update network route"
|
||||
switch (savedRoute.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update network route. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = savedRoute.error.data.message ? savedRoute.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: savedRoute.error.data ? savedRoute.error.data : savedRoute.error.message,
|
||||
content: errorMsg,
|
||||
key: saveKey,
|
||||
duration: 2,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(routeActions.setSavedRoute({...savedRoute, error: null}))
|
||||
@@ -168,7 +186,7 @@ export const Routes = () => {
|
||||
if (deletedRoute.loading) {
|
||||
message.loading({content: 'Deleting...', key: deleteKey, style})
|
||||
} else if (deletedRoute.success) {
|
||||
message.success({content: 'Route has been successfully disabled.', key: deleteKey, duration: 2, style})
|
||||
message.success({content: 'Route has been successfully deleted.', key: deleteKey, duration: 2, style})
|
||||
dispatch(routeActions.resetDeletedRoute(null))
|
||||
} else if (deletedRoute.error) {
|
||||
message.error({
|
||||
@@ -186,7 +204,7 @@ export const Routes = () => {
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peerIPToName)))
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
|
||||
}
|
||||
|
||||
const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => {
|
||||
@@ -224,7 +242,6 @@ export const Routes = () => {
|
||||
|
||||
|
||||
const onClickAddNewRoute = () => {
|
||||
dispatch(routeActions.setSetupNewRouteHA(true));
|
||||
dispatch(routeActions.setSetupNewRouteVisible(true));
|
||||
dispatch(routeActions.setRoute({
|
||||
network: '',
|
||||
@@ -261,10 +278,11 @@ export const Routes = () => {
|
||||
network: route.network,
|
||||
network_id: route.network_id,
|
||||
description: route.description,
|
||||
peer: route.peer ? peerToPeerIP(route.peer, peerNameToIP[route.peer]) : '',
|
||||
peer: route.peer ? peerToPeerIP(route.peer_name, route.peer_ip) : '',
|
||||
metric: route.metric ? route.metric : 9999,
|
||||
masquerade: route.masquerade,
|
||||
enabled: route.enabled
|
||||
enabled: route.enabled,
|
||||
groups: route.groups
|
||||
} as Route))
|
||||
dispatch(routeActions.setSetupNewRouteVisible(true));
|
||||
}
|
||||
@@ -293,17 +311,77 @@ export const Routes = () => {
|
||||
});
|
||||
}
|
||||
|
||||
const onPopoverVisibleChange = () => {
|
||||
if (setupNewRouteVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
} else {
|
||||
setGroupPopupVisible(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSwitchMasquerade(routeGroup: GroupedDataTable, checked: boolean) {
|
||||
routeGroup.groupedRoutes.forEach((record) => {
|
||||
const route = {
|
||||
...record,
|
||||
peer: peerNameToIP[record.peer],
|
||||
masquerade: checked,
|
||||
} as Route
|
||||
groupsToCreate: []
|
||||
} as RouteToSave
|
||||
dispatch(routeActions.saveRoute.request({getAccessTokenSilently: getAccessTokenSilently, payload: route}));
|
||||
})
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, rowGroups: string[] | null, userToAction: RouteDataTable) => {
|
||||
|
||||
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={() => setRouteAndView(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 expandedRowRender = (record: GroupedDataTable) => {
|
||||
|
||||
return <Table
|
||||
@@ -315,7 +393,7 @@ export const Routes = () => {
|
||||
size="small"
|
||||
bordered={true}
|
||||
>
|
||||
<Column title="Routing Peer" dataIndex="peer" align="center"
|
||||
<Column title="Routing Peer" dataIndex="peer_name" align="center"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).peer.includes(value)}
|
||||
sorter={(a, b) => ((a as any).peer.localeCompare((b as any).peer))}
|
||||
render={(text, record) => {
|
||||
@@ -327,7 +405,12 @@ export const Routes = () => {
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).metric.includes(value)}
|
||||
sorter={(a, b) => ((a as any).metric - ((b as any).metric))}
|
||||
/>
|
||||
<Column title="Status" dataIndex="enabled" align="center"
|
||||
<Column title="Groups" dataIndex="groupsCount" align="center"
|
||||
render={(text, record: RouteDataTable) => {
|
||||
return renderPopoverGroups(text, record.groups, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="Routing peer status" dataIndex="enabled" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="green">enabled</Tag> : <Tag color="red">disabled</Tag>
|
||||
}}
|
||||
@@ -336,7 +419,7 @@ export const Routes = () => {
|
||||
render={(text, record) => {
|
||||
if (deletedRoute.loading || savedRoute.loading) return <></>
|
||||
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
|
||||
onVisibleChange={visible => {
|
||||
onOpenChange={visible => {
|
||||
if (visible) setRouteToAction(record as RouteDataTable)
|
||||
}}></Dropdown.Button>
|
||||
}}
|
||||
@@ -438,7 +521,7 @@ export const Routes = () => {
|
||||
sorter={(a, b) => ((a as any).network.localeCompare((b as any).network))}
|
||||
// defaultSortOrder='ascend'
|
||||
/>
|
||||
<Column title="Status" dataIndex="enabled" align="center"
|
||||
<Column title="Route status" dataIndex="enabled" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="green">enabled</Tag> :
|
||||
<Tag color="red">disabled</Tag>
|
||||
@@ -463,8 +546,8 @@ export const Routes = () => {
|
||||
if (count > 1) {
|
||||
tag = <Tag color="green">on</Tag>
|
||||
}
|
||||
return <div>{tag}<Divider type="vertical"/><Button type="link"
|
||||
onClick={() => setRouteAndView(record)}>Configure</Button>
|
||||
return <div>{tag}<Divider type="vertical"/>
|
||||
<Button type="link" onClick={() => setRouteAndView(record)}>Configure</Button>
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -28,7 +28,7 @@ import UserUpdate from "../components/UserUpdate";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {Group} from "../store/group/types";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {useOidcIdToken, useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {Link} from "react-router-dom";
|
||||
import {actions as setupKeyActions} from "../store/setup-key";
|
||||
import {SetupKey} from "../store/setup-key/types";
|
||||
@@ -46,6 +46,7 @@ const styleNotification = {marginTop: 85}
|
||||
export const Users = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {oidcUser} = useOidcUser();
|
||||
const {idTokenPayload} = useOidcIdToken()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
@@ -96,8 +97,12 @@ export const Users = () => {
|
||||
}, [textToSearch])
|
||||
|
||||
useEffect(() => {
|
||||
if (oidcUser && oidcUser.sub) {
|
||||
const found = users.find(u => u.id == oidcUser.sub)
|
||||
let runUser = oidcUser
|
||||
if (!oidcUser) {
|
||||
runUser = idTokenPayload
|
||||
}
|
||||
if (runUser && runUser.sub) {
|
||||
const found = users.find(u => u.id == runUser.sub)
|
||||
if (found) {
|
||||
setCurrentUser(found)
|
||||
}
|
||||
@@ -110,7 +115,7 @@ export const Users = () => {
|
||||
const filterDataTable = (): User[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f: User[] = filter(users, (f: User) =>
|
||||
((f.email || f.id).toLowerCase().includes(t) || f.name.includes(t) || f.role.includes(t) || t === "")
|
||||
((f.email || f.id).toLowerCase().includes(t) || f.name.toLowerCase().includes(t) || f.role.includes(t) || t === "")
|
||||
) as User[]
|
||||
return f
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user