mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
Feature/dns settings (#126)
Add DNS settings view to the DNS tab. Split the page into sub-tabs for Nameservers and DNS settings Added API calls to the new DNS settings API
This commit is contained in:
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[]
|
||||
}
|
||||
@@ -10,6 +10,7 @@ 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';
|
||||
@@ -29,5 +30,6 @@ sagaMiddleware.run(groupSagas);
|
||||
sagaMiddleware.run(routeSagas);
|
||||
sagaMiddleware.run(nameserverGroupSagas);
|
||||
sagaMiddleware.run(eventSagas);
|
||||
sagaMiddleware.run(dnsSettingsSagas);
|
||||
|
||||
export { apiClient, rootReducer, store };
|
||||
@@ -6,6 +6,7 @@ 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,
|
||||
@@ -15,5 +16,6 @@ export default {
|
||||
rule: RuleActions,
|
||||
route: RouteActions,
|
||||
nameserverGroup: NameServerGroupActions,
|
||||
event: EventActions
|
||||
event: EventActions,
|
||||
dnsSettings: DNSSettingsActions
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ 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,
|
||||
@@ -17,5 +18,6 @@ export default combineReducers({
|
||||
rule,
|
||||
route,
|
||||
nameserverGroup,
|
||||
event
|
||||
event,
|
||||
dnsSettings
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -77,25 +77,29 @@ export const Activity = () => {
|
||||
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 <Row> <Text>Group <Text type="secondary">{event.meta.group}</Text> added to peer</Text> </Row>
|
||||
return getActivityRow(event.meta.group,"added to peer")
|
||||
case "peer.group.delete":
|
||||
return <Row> <Text>Group <Text type="secondary">{event.meta.group}</Text> removed from peer</Text>
|
||||
</Row>
|
||||
return getActivityRow(event.meta.group,"removed from peer")
|
||||
case "user.group.add":
|
||||
return <Row> <Text>Group <Text type="secondary">{event.meta.group}</Text> added to user</Text> </Row>
|
||||
return getActivityRow(event.meta.group,"added to user")
|
||||
case "user.group.delete":
|
||||
return <Row> <Text>Group <Text type="secondary">{event.meta.group}</Text> removed from user</Text>
|
||||
</Row>
|
||||
return getActivityRow(event.meta.group,"removed from user")
|
||||
case "setupkey.group.add":
|
||||
return <Row> <Text>Group <Text type="secondary">{event.meta.group}</Text> added to setup key</Text>
|
||||
</Row>
|
||||
return getActivityRow(event.meta.group,"added to setup key")
|
||||
case "setupkey.group.delete":
|
||||
return <Row> <Text>Group <Text type="secondary">{event.meta.group}</Text> removed setup key</Text>
|
||||
</Row>
|
||||
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
|
||||
}
|
||||
@@ -126,6 +130,13 @@ export const Activity = () => {
|
||||
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 "-"
|
||||
@@ -138,62 +149,41 @@ export const Activity = () => {
|
||||
case "rule.add":
|
||||
case "rule.delete":
|
||||
case "rule.update":
|
||||
return <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{event.meta.name}</Text> </Row>
|
||||
<Row> <Text type="secondary">Rule</Text> </Row>
|
||||
</span>
|
||||
return renderMultiRowSpan(event.meta.name,"Rule")
|
||||
case "setupkey.add":
|
||||
case "setupkey.revoke":
|
||||
case "setupkey.update":
|
||||
case "setupkey.overuse":
|
||||
return <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{event.meta.name}</Text> </Row>
|
||||
<Row> <Text
|
||||
type="secondary">{capitalize(event.meta.type)} setup key ({event.meta.key})</Text> </Row>
|
||||
</span>
|
||||
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 <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{event.meta.name}</Text> </Row>
|
||||
<Row> <Text type="secondary">Group</Text> </Row>
|
||||
</span>
|
||||
return renderMultiRowSpan(event.meta.name,"Group")
|
||||
case "setupkey.peer.add":
|
||||
case "user.peer.add":
|
||||
case "user.peer.delete":
|
||||
return <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{event.meta.fqdn}</Text> </Row>
|
||||
<Row> <Text type="secondary">{event.meta.ip}</Text> </Row>
|
||||
</span>
|
||||
return renderMultiRowSpan(event.meta.fqdn,event.meta.ip)
|
||||
case "user.group.add":
|
||||
case "user.group.delete":
|
||||
if (user) {
|
||||
return <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 renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
|
||||
}
|
||||
return "n/a"
|
||||
case "setupkey.group.add":
|
||||
case "setupkey.group.delete":
|
||||
return <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{event.meta.setupkey}</Text> </Row>
|
||||
<Row> <Text type="secondary">Setup Key</Text> </Row>
|
||||
</span>
|
||||
|
||||
return renderMultiRowSpan(event.meta.setupkey,"Setup Key")
|
||||
case "peer.group.add":
|
||||
case "peer.group.delete":
|
||||
return <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{event.meta.peer_fqdn}</Text> </Row>
|
||||
<Row> <Text type="secondary">{event.meta.peer_ip}</Text> </Row>
|
||||
</span>
|
||||
return renderMultiRowSpan(event.meta.peer_fqdn,event.meta.peer_ip)
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
return "-"
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
return "-"
|
||||
case "user.invite":
|
||||
if (user) {
|
||||
return <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 renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return event.target_id
|
||||
|
||||
@@ -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 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 = [
|
||||
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;
|
||||
Reference in New Issue
Block a user