Compare commits

...

7 Commits

Author SHA1 Message Date
Givi Khojanashvili
e57e5b726d Feat add custom id claim (#129)
Some checks failed
build and push / build_n_push (push) Has been cancelled
* Fix management API endpoint ENV var. Format README.

* Add and use id_current user flag

* Use mix of the new and old methods to detect current user.
2023-02-03 21:48:03 +01:00
Misha Bragin
2c4ada0ad8 Use peerID in the Routes view (#130)
Relates to the netbirdio/netbird PR
Use Peer.ID instead of Peer.Key as peer identifier (#664)
2023-02-03 10:34:43 +01:00
Misha Bragin
8195587c85 Handle additional activity events (#128)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-01-31 13:56:46 +01:00
Maycon Santos
adf6e1e71f Use ID token payload when oidcUser is nil (#127)
With some IDPs like MS Azure the
oidcUser is not being set, so we can fallback to id token
to validate UI and current user
2023-01-24 09:10:14 +01:00
Maycon Santos
b733a186ae Remove console.log in peers page (#125)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-01-19 14:55:42 +01:00
Maycon Santos
5d901470c2 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
2023-01-18 18:12:29 +01:00
Misha Bragin
29ab28847d Add Events view (#119) 2023-01-02 17:29:11 +01:00
33 changed files with 1440 additions and 508 deletions

View File

@@ -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`

View File

@@ -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/>

View File

@@ -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)
}

View File

@@ -73,14 +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 (!newRoute ) {
setRoutingPeerMSG("Add additional routing peer")
setRoutingPeerMSG(defaultRoutingPeerMSG)
setMasqueradeMSG("Update Masquerade")
setStatusMSG("Update Status")
} else {
@@ -136,9 +136,9 @@ const RouteUpdate = () => {
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)
@@ -167,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) => {

View File

@@ -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)

View 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;

View 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 };

View 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
});

View 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),
]);
}

View 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
);
},
};

View File

@@ -0,0 +1,8 @@
export interface DNSSettings {
disabled_management_groups: string[]
}
export interface DNSSettingsToSave extends DNSSettings
{
groupsToCreate: string[]
}

View 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
View 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 };

View 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
View 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),
]);
}

View 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
View 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 }
}

View File

@@ -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 };

View File

@@ -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,

View File

@@ -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[];

View File

@@ -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
};

View File

@@ -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
});

View File

@@ -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[]
}
}

View File

@@ -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(' ')
}

View File

@@ -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
}
}

View File

@@ -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 {
@@ -37,30 +41,34 @@ export interface GroupedDataTable {
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 groupList:string[] = []
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
@@ -69,8 +77,8 @@ export const transformGroupedDataTable = (routes:Route[],peerIPToName:PeerIPToNa
groupList = groupList.concat(r.groups)
}
})
groupList = groupList.filter((value,index,arrary) => arrary.indexOf(value) === index)
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,

272
src/views/Activity.tsx Normal file
View 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;

View File

@@ -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
View 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
View 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;

View File

@@ -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,6 @@ export const Peers = () => {
<Column title="LastSeen" dataIndex="last_seen"
render={(text, record, index) => {
console.log(text)
let dt = new Date(text)
return <Popover content={dt.toLocaleString()}>
{(record as PeerDataTable).connected ? 'just now' : timeAgo(text)}

View File

@@ -127,7 +127,7 @@ export const Routes = () => {
}
useEffect(() => {
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peerIPToName)))
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
}, [dataTable])
useEffect(() => {
@@ -135,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}
@@ -186,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({
@@ -204,7 +204,7 @@ export const Routes = () => {
};
const searchDataTable = () => {
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peerIPToName)))
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
}
const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => {
@@ -278,7 +278,7 @@ 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,
@@ -393,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) => {

View File

@@ -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
}