Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable/Disable SSH server in the peers table #54

Merged
merged 11 commits into from
Jun 23, 2022
2 changes: 1 addition & 1 deletion src/components/addpeer/LinuxTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const OtherTab = () => {
const [steps, _] = useState([
{
key: 1,
title: 'For different installation options check out our documentation.',
title: 'For other installation options check our documentation.',
commands: (
<Button type="primary" href={`https://netbird.io/docs/getting-started/installation#binary-install`} target="_blank">
Documentation
Expand Down
7 changes: 6 additions & 1 deletion src/store/group/actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import { Group } from './types';
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
import {
ApiError,
CreateResponse,
DeleteResponse,
RequestPayload
} from '../../services/api-client/types';

const actions = {
getGroups: createAsyncAction(
Expand Down
17 changes: 15 additions & 2 deletions src/store/peer/actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import {Peer, PeerGroupsToSave} from './types';
import {ApiError, ChangeResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
import {
ApiError,
ChangeResponse,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import of CreateResponse seems to be left over of local development

CreateResponse,
DeleteResponse,
RequestPayload
} from '../../services/api-client/types';
import {Group} from "../group/types";

const actions = {
Expand Down Expand Up @@ -28,7 +34,14 @@ const actions = {

removePeer: createAction('REMOVE_PEER')<string>(),
setPeer: createAction('SET_PEER')<Peer | null>(),
setUpdateGroupsVisible: createAction('SET_UPDATE_GROUPS_VISIBLE')<boolean>()
setUpdateGroupsVisible: createAction('SET_UPDATE_GROUPS_VISIBLE')<boolean>(),
updatePeer: createAsyncAction(
'UPDATE_PEER',
'UPDATE_PEER_SUCCESS',
'UPDATE_PEER_FAILURE',
)<RequestPayload<Peer>, ChangeResponse<Peer | null>, ChangeResponse<Peer | null>>(),
setUpdatedPeer: createAction('SET_UPDATED_PEER')<ChangeResponse<Peer | null>>(),
resetUpdatedPeer: createAction('RESET_UPDATED_PEER')<null>(),
};

export type ActionTypes = ActionType<typeof actions>;
Expand Down
22 changes: 19 additions & 3 deletions src/store/peer/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { Peer } from './types';
import actions, { ActionTypes } from './actions';
import {ApiError, ChangeResponse, DeleteResponse} from "../../services/api-client/types";
import {ApiError, ChangeResponse, CreateResponse, DeleteResponse} from "../../services/api-client/types";
import {Group} from "../group/types";

type StateType = Readonly<{
Expand All @@ -14,6 +14,7 @@ type StateType = Readonly<{
deletedPeer: DeleteResponse<string | null>;
setUpdateGroupsVisible: boolean;
savedGroups: ChangeResponse<Group[] | null>;
updatedPeer: CreateResponse<Peer | null>;
}>;

const initialState: StateType = {
Expand All @@ -36,7 +37,14 @@ const initialState: StateType = {
failure: false,
error: null,
data: null
}
},
updatedPeer: <ChangeResponse<Peer | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
};

const data = createReducer<Peer[], ActionTypes>(initialState.data as Peer[])
Expand Down Expand Up @@ -77,6 +85,13 @@ const savedGroups = createReducer<ChangeResponse<Group[] | null>, ActionTypes>(i
.handleAction(actions.saveGroups.failure, (store, action) => action.payload)
.handleAction(actions.resetSavedGroups, () => initialState.savedGroups)

const updatedPeer = createReducer<CreateResponse<Peer | null>, ActionTypes>(initialState.updatedPeer)
.handleAction(actions.updatePeer.request, () => initialState.updatedPeer)
.handleAction(actions.updatePeer.success, (store, action) => action.payload)
.handleAction(actions.updatePeer.failure, (store, action) => action.payload)
.handleAction(actions.setUpdatedPeer, (store, action) => action.payload)
.handleAction(actions.resetUpdatedPeer, () => initialState.updatedPeer)

export default combineReducers({
data,
peer,
Expand All @@ -85,5 +100,6 @@ export default combineReducers({
saving,
deletedPeer,
updateGroupsVisible,
savedGroups
savedGroups,
updatedPeer
});
50 changes: 47 additions & 3 deletions src/store/peer/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import {
ApiResponse,
ChangeResponse,
CreateResponse,
DeleteResponse,
RequestPayload
DeleteResponse
} from '../../services/api-client/types';
import { Peer } from './types'
import service from './service';
Expand Down Expand Up @@ -153,11 +152,56 @@ export function* saveGroups(action: ReturnType<typeof actions.saveGroups.request
}
}

export function* updatePeer(action: ReturnType<typeof actions.updatePeer.request>): Generator {
try {
yield put(actions.setUpdatedPeer({
loading: true,
success: false,
failure: false,
error: null,
data: null
}))

const peer = action.payload.payload
const peerId = peer.id

const payloadToSave = {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: peer
}

const effect = yield call(service.updatePeer, payloadToSave)
const response = effect as ApiResponse<Peer>;

yield put(actions.updatePeer.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as ChangeResponse<Peer | null>));

const peers = (yield select(state => state.peer.data)) as Peer[]
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.id !== peerId).concat(response.body)))

} catch (err) {
console.log(err)
yield put(actions.updatePeer.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as ChangeResponse<Peer | null>));
}
}

export default function* sagas(): Generator {
yield all([
takeLatest(actions.getPeers.request, getPeers),
takeLatest(actions.deletedPeer.request, deletePeer),
takeLatest(actions.saveGroups.request, saveGroups)
takeLatest(actions.saveGroups.request, saveGroups),
takeLatest(actions.updatePeer.request, updatePeer)
]);
}

8 changes: 8 additions & 0 deletions src/store/peer/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,13 @@ export default {
`/api/peers/` + payload.payload,
payload
);
},
async updatePeer(payload:RequestPayload<Peer>): Promise<ApiResponse<Peer>> {
const id = payload.payload.id
delete payload.payload.id
return apiClient.put<Peer>(
`/api/peers/${id}`,
payload
);
}
};
1 change: 1 addition & 0 deletions src/store/peer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface Peer {
os: string,
version: string,
groups?: Group[]
ssh_enabled: boolean,
}

export interface PeerToSave extends Peer {
Expand Down
2 changes: 1 addition & 1 deletion src/store/rule/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { Rule } from './types';
import actions, { ActionTypes } from './actions';
import {ApiError, DeleteResponse, CreateResponse, ChangeResponse} from "../../services/api-client/types";
import {ApiError, DeleteResponse, CreateResponse} from "../../services/api-client/types";

type StateType = Readonly<{
data: Rule[] | null;
Expand Down
3 changes: 0 additions & 3 deletions src/views/AccessControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,9 @@ import {CloseOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
import bidirect from '../assets/direct_bi.svg';
import inbound from '../assets/direct_in.svg';
import outbound from '../assets/direct_out.svg';
import tutorial from "../assets/access_control_tutorial.svg";
import AccessControlNew from "../components/AccessControlNew";
import {Group} from "../store/group/types";
import {actions as setupKeyActions} from "../store/setup-key";
import AccessControlModalGroups from "../components/AccessControlModalGroups";
import TableSpin from "../components/Spin";
import tableSpin from "../components/Spin";

const { Title, Paragraph } = Typography;
Expand Down
114 changes: 91 additions & 23 deletions src/views/Peers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,37 @@ import React, {useEffect, useState} from 'react';
import {Link} from 'react-router-dom';
import {useDispatch, useSelector} from "react-redux";
import {useAuth0, withAuthenticationRequired} from "@auth0/auth0-react";
import { RootState } from "typesafe-actions";
import { actions as peerActions } from '../store/peer';
import { actions as groupActions } from '../store/group';
import {RootState} from "typesafe-actions";
import {actions as peerActions} from '../store/peer';
import {actions as groupActions} from '../store/group';
import Loading from "../components/Loading";
import {Container} from "../components/Container";
import {
Col,
Row,
Typography,
Table,
Alert,
Button,
Card,
Tag,
Col,
Dropdown,
Input,
Space,
Menu,
message,
Modal,
Popover,
Radio,
RadioChangeEvent,
Dropdown,
Menu,
Alert, Select, Modal, Button, message, Popover, SpinProps, Spin
Row,
Select,
Space,
Switch,
Table,
Tag,
Typography,
Tooltip
} from "antd";
import {Peer} from "../store/peer/types";
import {filter} from "lodash"
import {formatOS, timeAgo} from "../utils/common";
import {ExclamationCircleOutlined} from "@ant-design/icons";
import Icon, {ExclamationCircleOutlined, QuestionCircleOutlined, WarningOutlined} from "@ant-design/icons";
import ButtonCopyMessage from "../components/ButtonCopyMessage";
import {Group, GroupPeer} from "../store/group/types";
import PeerGroupsUpdate from "../components/PeerGroupsUpdate";
Expand Down Expand Up @@ -53,6 +60,7 @@ export const Peers = () => {
const groups = useSelector((state: RootState) => state.group.data);
const loadingGroups = useSelector((state: RootState) => state.group.loading);
const savedGroups = useSelector((state: RootState) => state.peer.savedGroups);
const updatedPeer = useSelector((state: RootState) => state.peer.updatedPeer);

const [textToSearch, setTextToSearch] = useState('');
const [optionOnOff, setOptionOnOff] = useState('all');
Expand Down Expand Up @@ -133,6 +141,22 @@ export const Peers = () => {
}
}, [savedGroups])

const updatePeerKey = 'updating_peer';
useEffect(() => {
const style = { marginTop: 85 }
if (updatedPeer.loading) {
message.loading({ content: 'Updating peer...', key: updatePeerKey, duration: 0, style })
} else if (updatedPeer.success) {
message.success({ content: 'Peer has been successfully updated.', key: updatePeerKey, duration: 2, style });
dispatch(peerActions.setUpdatedPeer({ ...updatedPeer, success: false }))
dispatch(peerActions.resetUpdatedPeer(null))
} else if (updatedPeer.error) {
message.error({ content: 'Failed to update peer. You might not have enough permissions.', key: updatePeerKey, duration: 2, style });
dispatch(peerActions.setUpdatedPeer({ ...updatedPeer, error: null }))
dispatch(peerActions.resetUpdatedPeer(null))
}
}, [updatedPeer])

const filterDataTable = ():Peer[] => {
const t = textToSearch.toLowerCase().trim()
let f:Peer[] = filter(peers, (f:Peer) =>
Expand Down Expand Up @@ -162,17 +186,12 @@ export const Peers = () => {
}

const showConfirmDelete = () => {
let name = peerToAction ? peerToAction.name : ''
confirm({
icon: <ExclamationCircleOutlined />,
title: "Delete peer \"" + name + "\"",
width: 600,
content: <Space direction="vertical" size="small">
{peerToAction &&
<>
<Title level={5}>Delete peer "{peerToAction ? peerToAction.name : ''}"</Title>
<Paragraph>Are you sure you want to delete peer from your account?</Paragraph>
</>
}
</Space>,
content: "Are you sure you want to delete peer from your account?",
okType: 'danger',
onOk() {
dispatch(peerActions.deletedPeer.request({getAccessTokenSilently, payload: peerToAction ? peerToAction.ip : ''}));
Expand All @@ -183,6 +202,29 @@ export const Peers = () => {
});
}

const showConfirmEnableSSH = (record: PeerDataTable) => {
confirm({
icon: <ExclamationCircleOutlined />,
title: "Enable SSH Server for \"" + record.name + "\"?",
width: 600,
content: "Experimental feature. Enabling this option allows remote SSH access to this machine from other connected network participants.",
okType: 'danger',
onOk() {
handleSwitchSSH(record, true)
},
onCancel() {
},
});
}
function handleSwitchSSH(record: PeerDataTable, checked: boolean) {
const peer = {
id: record.id,
ssh_enabled: checked,
name: record.name
} as Peer
dispatch(peerActions.updatePeer.request({getAccessTokenSilently, payload: peer}));

}
const setUpdateGroupsVisible = (peerToAction:Peer, status:boolean) => {
if (status) {
dispatch(peerActions.setPeer({...peerToAction}))
Expand Down Expand Up @@ -288,16 +330,42 @@ export const Peers = () => {
return <ButtonCopyMessage keyMessage={(record as PeerDataTable).key} text={text} messageText={'IP copied!'} styleNotification={{}}/>
}}
/>
<Column title="Status" dataIndex="connected"
<Column title="Status" dataIndex="connected" align="center"
render={(text, record, index) => {
return text ? <Tag color="green">online</Tag> : <Tag color="red">offline</Tag>
}}
/>
<Column title="Groups" dataIndex="groupsCount"
<Column title="Groups" dataIndex="groupsCount" align="center"
render={(text, record:PeerDataTable, index) => {
return renderPopoverGroups(text, record.groups, record)
}}
/>
<Column
title="SSH Server" dataIndex="ssh_enabled" align="center"
render={(e, record:PeerDataTable, index) => {
let isWindows = record.os.toLocaleLowerCase().startsWith("windows")
let toggle = <Switch size={"small"} checked={e}
disabled={isWindows}
onClick={(checked: boolean) => {
if (checked) {
showConfirmEnableSSH(record)
} else {
handleSwitchSSH(record, checked)
}
}}
/>

if (isWindows) {
return <Tooltip title="SSH server feature is not yet supported on Windows">
{toggle}
</Tooltip>
} else {
return toggle
}
}
}
/>

<Column title="LastSeen" dataIndex="last_seen"
render={(text, record, index) => {
return (record as PeerDataTable).connected ? 'just now' : timeAgo(text)
Expand Down