From 6724e5bfaabdb5662ef48de2c965a1260e41a1a3 Mon Sep 17 00:00:00 2001 From: Misha Bragin Date: Wed, 15 Jun 2022 11:42:15 +0200 Subject: [PATCH] Feature/access control (#49) Add Access control feature page Users can create and update access control rules from the UI Users can assign groups to peers Minor enhancements to the UI Co-authored-by: Raphael Oliveira Co-authored-by: DataOnTabs <68952619+dataontabs@users.noreply.github.com> Co-authored-by: mlsmaycon --- src/App.tsx | 6 +- src/components/AccessControlModalGroups.tsx | 6 +- src/components/AccessControlNew.tsx | 153 ++++++++--- src/components/Navbar.tsx | 8 +- src/components/PeerGroupsUpdate.tsx | 193 +++++++++++++ src/components/SetupKeyNew.tsx | 4 +- src/components/Spin.tsx | 12 + src/index.css | 12 +- src/index.tsx | 13 - src/store/group/sagas.ts | 4 +- src/store/group/service.ts | 4 +- src/store/group/types.ts | 13 +- src/store/peer/actions.ts | 19 +- src/store/peer/reducer.ts | 31 ++- src/store/peer/sagas.ts | 102 ++++++- src/store/peer/types.ts | 28 +- src/store/rule/actions.ts | 3 + src/store/rule/reducer.ts | 4 +- src/store/rule/sagas.ts | 23 +- src/store/rule/service.ts | 4 +- src/store/rule/types.ts | 12 +- src/store/setup-key/actions.ts | 5 +- src/store/setup-key/reducer.ts | 4 +- src/store/setup-key/sagas.ts | 21 +- src/store/setup-key/service.ts | 4 +- src/store/setup-key/types.ts | 30 +-- src/views/AccessControl.tsx | 153 ++++++----- src/views/Peers.tsx | 284 ++++++++++++-------- src/views/SetupKeys.tsx | 72 ++--- src/views/Users.tsx | 4 +- 30 files changed, 894 insertions(+), 337 deletions(-) create mode 100644 src/components/PeerGroupsUpdate.tsx create mode 100644 src/components/Spin.tsx diff --git a/src/App.tsx b/src/App.tsx index 0aed4ca..168d0ae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,8 @@ import Loading from "./components/Loading"; import SetupKeys from "./views/SetupKeys"; import AddPeer from "./views/AddPeer"; import Users from './views/Users'; +import AccessControl from './views/AccessControl'; +// import Activity from './views/Activity'; import Banner from "./components/Banner"; import {store} from "./store"; @@ -111,8 +113,8 @@ function App() { - {/* - */} + + {/**/} diff --git a/src/components/AccessControlModalGroups.tsx b/src/components/AccessControlModalGroups.tsx index f7525ae..e06e50d 100644 --- a/src/components/AccessControlModalGroups.tsx +++ b/src/components/AccessControlModalGroups.tsx @@ -27,9 +27,9 @@ const AccessControlModalGroups:React.FC = ({data, title, visible, onCance renderItem={(item:Group) => ( {item.Name.slice(0,1).toUpperCase()}} - title={item.Name} - description={`${item.PeersCount} peers`} + avatar={{item.name.slice(0,1).toUpperCase()}} + title={item.name} + description={`${item.peers_count} peers`} /> )} diff --git a/src/components/AccessControlNew.tsx b/src/components/AccessControlNew.tsx index a959790..7f01dad 100644 --- a/src/components/AccessControlNew.tsx +++ b/src/components/AccessControlNew.tsx @@ -2,19 +2,16 @@ import React, {useEffect, useRef, useState} from 'react'; import {useDispatch, useSelector} from "react-redux"; import {RootState} from "typesafe-actions"; import { actions as ruleActions } from '../store/rule'; -import { actions as groupsActions } from '../store/group'; -import inbound from '../assets/direct_in.svg'; -import outbound from '../assets/direct_out.svg'; import { Col, Row, Typography, Input, Space, - Radio, - Button, Drawer, Form, List, Divider, Select, Tag + Switch, + Button, Drawer, Form, Divider, Select, Tag, Radio, RadioChangeEvent } from "antd"; -import {ArrowRightOutlined, CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons"; +import {ArrowRightOutlined, CheckOutlined, CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons"; import type { CustomTagProps } from 'rc-select/lib/BaseSelect' import {Rule, RuleToSave} from "../store/rule/types"; import {useAuth0} from "@auth0/auth0-react"; @@ -38,10 +35,14 @@ const AccessControlNew = () => { const savedRule = useSelector((state: RootState) => state.rule.savedRule) const [editName, setEditName] = useState(false) + const [editDescription, setEditDescription] = useState(false) const [tagGroups, setTagGroups] = useState([] as string[]) const [formRule, setFormRule] = useState({} as FormRule) const [form] = Form.useForm() const inputNameRef = useRef(null) + const inputDescriptionRef = useRef(null) + + const optionsDisabledEnabled = [{label: 'Enabled', value: false}, {label: 'Disabled', value: true}] useEffect(() => { if (editName) inputNameRef.current!.focus({ @@ -49,36 +50,44 @@ const AccessControlNew = () => { }); }, [editName]); + useEffect(() => { + if (editDescription) inputDescriptionRef.current!.focus({ + cursor: 'end', + }); + }, [editDescription]); + useEffect(() => { if (!rule) return const fRule = { ...rule, - tagSourceGroups: rule.Source ? rule.Source?.map(t => t.Name) : [], - tagDestinationGroups: rule.Destination ? rule.Destination?.map(t => t.Name) : [] + tagSourceGroups: rule.sources ? rule.sources?.map(t => t.name) : [], + tagDestinationGroups: rule.destinations ? rule.destinations?.map(t => t.name) : [] } as FormRule setFormRule(fRule) form.setFieldsValue(fRule) }, [rule]) useEffect(() => { - setTagGroups(groups?.map(g => g.Name) || []) + setTagGroups(groups?.map(g => g.name) || []) }, [groups]) const createRuleToSave = ():RuleToSave => { - const Source = groups?.filter(g => formRule.tagSourceGroups.includes(g.Name)).map(g => g.ID || '') || [] - const Destination = groups?.filter(g => formRule.tagDestinationGroups.includes(g.Name)).map(g => g.ID || '') || [] + const sources = groups?.filter(g => formRule.tagSourceGroups.includes(g.name)).map(g => g.id || '') || [] + const destinations = groups?.filter(g => formRule.tagDestinationGroups.includes(g.name)).map(g => g.id || '') || [] const sourcesNoId = formRule.tagSourceGroups.filter(s => !tagGroups.includes(s)) const destinationsNoId = formRule.tagDestinationGroups.filter(s => !tagGroups.includes(s)) const groupsToSave = uniq([...sourcesNoId, ...destinationsNoId]) return { - ID: formRule.ID, - Name: formRule.Name, - Source, - Destination, + id: formRule.id, + name: formRule.name, + description: formRule.description, + sources, + destinations, sourcesNoId, destinationsNoId, groupsToSave, - Flow: formRule.Flow + flow: formRule.flow, + disabled: formRule.disabled } as RuleToSave } @@ -101,10 +110,12 @@ const AccessControlNew = () => { if (savedRule.loading) return setEditName(false) dispatch(ruleActions.setRule({ - Name: '', - Source: [], - Destination: [], - Flow: 'bidirect' + name: '', + description: '', + sources: [], + destinations: [], + flow: 'bidirect', + disabled: false } as Rule)) setVisibleNewRule(false) } @@ -127,6 +138,13 @@ const AccessControlNew = () => { }) }; + const handleChangeDisabled = ({ target: { value } }: RadioChangeEvent) => { + setFormRule({ + ...formRule, + disabled: value + }) + }; + const tagRender = (props: CustomTagProps) => { const { label, value, closable, onClose } = props; const onPreventMouseDown = (event: React.MouseEvent) => { @@ -149,8 +167,8 @@ const AccessControlNew = () => { const optionRender = (label: string) => { let peersCount = '' - const g = groups.find(_g => _g.Name === label) - if (g) peersCount = ` - ${g.PeersCount || 0} ${(g.PeersCount && parseInt(g.PeersCount) > 1) ? 'peers' : 'peer'} ` + const g = groups.find(_g => _g.name === label) + if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} ` return ( <> { ) } + const dropDownRender = (menu: React.ReactElement) => ( + <> + {menu} + + + + Add new group by pressing "Enter" + + + + + + + + + ) + const toggleEditName = (status:boolean) => { setEditName(status); } + const toggleEditDescription = (status:boolean) => { + setEditDescription(status); + } + // const testDeleteGroup = () => { // groups.forEach(g => { // dispatch(groupsActions.deleteGroup.request({getAccessTokenSilently, payload: g.ID || ''})) @@ -185,10 +224,11 @@ const AccessControlNew = () => { visible={setupNewRuleVisible} bodyStyle={{paddingBottom: 80}} onClose={onCancel} + autoFocus={true} footer={ - + } > @@ -198,7 +238,7 @@ const AccessControlNew = () => {
- {!editName && formRule.ID && + {!editName && !editDescription && formRule.id &&
+ + + + + {/*} + unCheckedChildren={} + + onChange={handleChangeDisabled} + />*/} + + + { rules={[{required: true, message: 'Please enter ate least one group'}]} style={{display: 'flex'}} > - { tagGroups.map(m => @@ -250,7 +334,13 @@ const AccessControlNew = () => { rules={[{required: true, message: 'Please enter ate least one group'}]} style={{display: 'flex'}} > - { tagGroups.map(m => @@ -271,7 +361,6 @@ const AccessControlNew = () => { If you want to enable all peers of the same group to talk to each other - you can add that group both as a receiver and as a destination. - Learn more about access control... @@ -279,7 +368,7 @@ const AccessControlNew = () => { + more about access controls diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 01dbbad..2d9cd30 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -5,8 +5,8 @@ import {useAuth0} from "@auth0/auth0-react"; import {useLocation} from 'react-router-dom'; import {Menu, Row, Col, Grid, Dropdown, Avatar, Button, Typography, Space} from 'antd' import {ItemType} from "antd/lib/menu/hooks/useItems"; -import {UserOutlined} from "@ant-design/icons"; import {AvatarSize} from "antd/es/avatar/SizeContext"; +import { UserOutlined } from '@ant-design/icons'; const { Text } = Typography const { useBreakpoint } = Grid; @@ -30,8 +30,8 @@ const Navbar = () => { { label: (Peers), key: '/peers' }, { label: (Add Peer), key: '/add-peer' }, { label: (Setup Keys), key: '/setup-keys' }, - /*{ label: (Access Control), key: '/acls' }, - { label: (Activity), key: '/activity' },*/ + { label: (Access Control), key: '/acls' }, + // { label: (Activity), key: '/activity' }, { label: (Users), key: '/users' } ] as ItemType[]) @@ -80,7 +80,7 @@ const Navbar = () => { const createAvatar = (size:AvatarSize) => { return user?.picture ? ( - + } /> ) : ( {(user?.name || '').slice(0, 1).toUpperCase()} ) diff --git a/src/components/PeerGroupsUpdate.tsx b/src/components/PeerGroupsUpdate.tsx new file mode 100644 index 0000000..7f800da --- /dev/null +++ b/src/components/PeerGroupsUpdate.tsx @@ -0,0 +1,193 @@ +import React, {useEffect, useRef, useState} from 'react'; +import {useDispatch, useSelector} from "react-redux"; +import {RootState} from "typesafe-actions"; +import { actions as peerActions } from '../store/peer'; +import { + Col, + Row, + Typography, + Space, + Button, Drawer, Form, Select, Tag, Divider +} from "antd"; +import type { CustomTagProps } from 'rc-select/lib/BaseSelect' +import {useAuth0} from "@auth0/auth0-react"; +import {PeerGroupsToSave} from "../store/peer/types"; +import {Group, GroupPeer} from "../store/group/types"; + +const { Paragraph } = Typography; +const { Option } = Select; + +const PeerGroupsUpdate = () => { + const { getAccessTokenSilently } = useAuth0() + const dispatch = useDispatch() + const groups = useSelector((state: RootState) => state.group.data) + const peer = useSelector((state: RootState) => state.peer.peer) + const updateGroupsVisible = useSelector((state: RootState) => state.peer.updateGroupsVisible) + const savedGroups = useSelector((state: RootState) => state.peer.savedGroups) + + const [tagGroups, setTagGroups] = useState([] as string[]) + const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[]) + const [peerGroups, setPeerGroups] = useState([] as GroupPeer[]) + const [peerGroupsToSave, setPeerGroupsToSave] = useState({ + ID: '', + groupsNoId: [], + groupsToSave: [], + groupsToRemove: [], + groupsToAdd: [] + } as PeerGroupsToSave) + + const [form] = Form.useForm() + + useEffect(() => { + if (!peer) return + const gs = peer?.groups?.map(g => ({id: g?.id || '', name: g.name} as GroupPeer)) as GroupPeer[] + const gs_name = gs?.map(g => g.name) as string[] + setPeerGroups(gs) + setSelectedTagGroups(gs_name) + form.setFieldsValue({ + groups: gs_name + }) + }, [peer]) + + useEffect(() => { + setTagGroups(groups?.map(g => g.name) || []) + }, [groups]) + + useEffect(() => { + const groupsToRemove = peerGroups.filter(pg => !selectedTagGroups.includes(pg.name)).map(g => g.id) + const groupsToAdd = (groups as Group[]).filter(g => selectedTagGroups.includes(g.name) && !groupsToRemove.includes(g.id || '') && !peerGroups.find(pg => pg.id === g.id)).map(g => g.id) as string[] + const groupsNoId = selectedTagGroups.filter(stg => !groups.find(g => g.name === stg)) + setPeerGroupsToSave({ + ...peerGroupsToSave, + ID: peer?.id || '', + groupsToRemove, + groupsToAdd, + groupsNoId + }) + }, [selectedTagGroups]) + + const tagRender = (props: CustomTagProps) => { + const { label, value, closable, onClose } = props; + const onPreventMouseDown = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + + return ( + + {value} + + ); + } + + const optionRender = (label: string) => { + let peersCount = '' + const g = groups.find(_g => _g.name === label) + if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} ` + return ( + <> + + {label} + + {peersCount} + + ) + } + + const dropDownRender = (menu: React.ReactElement) => ( + <> + {menu} + + + + Add new group by pressing "Enter" + + + + + + + + + ) + + const setUpdateGroupsVisible = (status:boolean) => { + dispatch(peerActions.setUpdateGroupsVisible(status)); + } + + const onCancel = () => { + dispatch(peerActions.setPeer(null)) + setUpdateGroupsVisible(false) + } + + const onChange = (data:any) => { + //setFormRule({...formRule, ...data}) + } + + const handleChangeTags = (value: string[]) => { + setSelectedTagGroups(value) + }; + + const handleFormSubmit = () => { + form.validateFields() + .then((values) => { + dispatch(peerActions.saveGroups.request({getAccessTokenSilently, payload: peerGroupsToSave})) + }) + .catch((errorInfo) => { + console.log('errorInfo', errorInfo) + }); + } + + return ( + <> + {peer && + + + + + } + > +
+ + + + + + + +
+
+ } + + ) +} + +export default PeerGroupsUpdate \ No newline at end of file diff --git a/src/components/SetupKeyNew.tsx b/src/components/SetupKeyNew.tsx index 9ef2775..a01f4df 100644 --- a/src/components/SetupKeyNew.tsx +++ b/src/components/SetupKeyNew.tsx @@ -48,8 +48,8 @@ const SetupKeyNew = () => { const onCancel = () => { if (createdSetupKey.loading) return dispatch(setupKeyActions.setSetupKey({ - Name: '', - Type: 'reusable' + name: '', + type: 'reusable' } as SetupKey)) setVisibleNewSetupKey(false) } diff --git a/src/components/Spin.tsx b/src/components/Spin.tsx new file mode 100644 index 0000000..9f532c5 --- /dev/null +++ b/src/components/Spin.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import {SpinProps} from "antd"; + +const TableSpin = (loading: boolean): SpinProps => { + return { + spinning: loading, + delay: 1, + size: "large" + } +} + +export default TableSpin; diff --git a/src/index.css b/src/index.css index a41b419..6d14817 100644 --- a/src/index.css +++ b/src/index.css @@ -122,7 +122,17 @@ body { color: rgba(0, 0, 0, .85) !important; } -.access-control.ant-drawer-title:hover { +.access-control-table .tooltip-label:hover { text-decoration: underline; cursor: pointer; +} + +.access-control.input-text:hover { + text-decoration: underline; + cursor: pointer; +} + +.access-control.ant-drawer-subtitle { + line-height: 22px; + margin: 24px 0; } \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index c9b337f..4b0c871 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,19 +25,6 @@ const providerConfig = { onRedirectCallback, }; -/* -ReactDOM.render( - - - - - - - , - document.getElementById('root') -); -*/ - const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); diff --git a/src/store/group/sagas.ts b/src/store/group/sagas.ts index f4d4bcf..d2d519f 100644 --- a/src/store/group/sagas.ts +++ b/src/store/group/sagas.ts @@ -39,7 +39,7 @@ export function* saveGroup(action: ReturnType) } as CreateResponse)) let effect - if (action.payload.payload.ID) { + if (action.payload.payload.id) { effect = yield call(service.editGroup, action.payload); } else { effect = yield call(service.createGroup, action.payload); @@ -94,7 +94,7 @@ export function* deleteGroup(action: ReturnType)); const rules = (yield select(state => state.rule.data)) as Group[] - yield put(actions.getGroups.success(rules.filter((p:Group) => p.ID !== action.payload.payload))) + yield put(actions.getGroups.success(rules.filter((p:Group) => p.id !== action.payload.payload))) } catch (err) { yield put(actions.deleteGroup.failure({ loading: false, diff --git a/src/store/group/service.ts b/src/store/group/service.ts index 2f9cce7..6444a21 100644 --- a/src/store/group/service.ts +++ b/src/store/group/service.ts @@ -29,8 +29,10 @@ export default { ); }, async editGroup(payload:RequestPayload): Promise> { + const id = payload.payload.id + delete payload.payload.id return apiClient.put( - `${baseUrl}`, + `${baseUrl}/${id}`, payload ); }, diff --git a/src/store/group/types.ts b/src/store/group/types.ts index 57c55b9..89ec5f9 100644 --- a/src/store/group/types.ts +++ b/src/store/group/types.ts @@ -1,6 +1,11 @@ export interface Group { - ID?: string; - Name: string; - Peers?: any[]; - PeersCount?: string; + id?: string; + name: string; + peers?: GroupPeer[] | string[]; + peers_count?: string; +} + +export interface GroupPeer { + id: string, + name: string } \ No newline at end of file diff --git a/src/store/peer/actions.ts b/src/store/peer/actions.ts index 6964bee..687cbc4 100644 --- a/src/store/peer/actions.ts +++ b/src/store/peer/actions.ts @@ -1,6 +1,7 @@ import { ActionType, createAction, createAsyncAction } from 'typesafe-actions'; -import { Peer } from './types'; -import {ApiError, DeleteResponse, RequestPayload} from '../../services/api-client/types'; +import {Peer, PeerGroupsToSave} from './types'; +import {ApiError, ChangeResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types'; +import {Group} from "../group/types"; const actions = { getPeers: createAsyncAction( @@ -8,14 +9,26 @@ const actions = { 'GET_PEERS_SUCCESS', 'GET_PEERS_FAILURE', ), Peer[], ApiError>(), + deletedPeer: createAsyncAction( 'DELETE_PEER_REQUEST', 'DELETE_PEER_SUCCESS', 'DELETE_PEER_FAILURE' ), DeleteResponse, DeleteResponse>(), + resetDeletedPeer: createAction('RESET_DELETED_PEER')(), setDeletePeer: createAction('SET_DELETE_PEER')>(), + + saveGroups: createAsyncAction( + 'SAVE_PEERS_GROUPS_REQUEST', + 'SAVE_PEERS_GROUPS_SUCCESS', + 'SAVE_PEERS_GROUPS_FAILURE', + ), ChangeResponse, ChangeResponse>(), + setSavedGroups: createAction('SET_SAVE_PEER_GROUPS')>(), + resetSavedGroups: createAction('RESET_SAVE_PEER_GROUPS')(), + removePeer: createAction('REMOVE_PEER')(), - setPeer: createAction('SET_PEER')(), + setPeer: createAction('SET_PEER')(), + setUpdateGroupsVisible: createAction('SET_UPDATE_GROUPS_VISIBLE')() }; export type ActionTypes = ActionType; diff --git a/src/store/peer/reducer.ts b/src/store/peer/reducer.ts index 1c05152..7bbaa5d 100644 --- a/src/store/peer/reducer.ts +++ b/src/store/peer/reducer.ts @@ -2,15 +2,18 @@ import { createReducer } from 'typesafe-actions'; import { combineReducers } from 'redux'; import { Peer } from './types'; import actions, { ActionTypes } from './actions'; -import {ApiError, DeleteResponse} from "../../services/api-client/types"; +import {ApiError, ChangeResponse, DeleteResponse} from "../../services/api-client/types"; +import {Group} from "../group/types"; type StateType = Readonly<{ data: Peer[] | null; - peer: Peer | null; + peer?: Peer | null; loading: boolean; failed: ApiError | null; saving: boolean; deletedPeer: DeleteResponse; + setUpdateGroupsVisible: boolean; + savedGroups: ChangeResponse; }>; const initialState: StateType = { @@ -25,6 +28,14 @@ const initialState: StateType = { failure: false, error: null, data : null + }, + setUpdateGroupsVisible: false, + savedGroups: > { + loading: false, + success: false, + failure: false, + error: null, + data: null } }; @@ -32,7 +43,7 @@ const data = createReducer(initialState.data as Peer[]) .handleAction(actions.getPeers.success,(_, action) => action.payload) .handleAction(actions.getPeers.failure, () => []); -const peer = createReducer(initialState.peer as Peer) +const peer = createReducer(initialState.peer as Peer) .handleAction(actions.setPeer, (store, action) => action.payload); const loading = createReducer(initialState.loading) @@ -55,6 +66,16 @@ const deletedPeer = createReducer, ActionTypes>(in .handleAction(actions.deletedPeer.success, (store, action) => action.payload) .handleAction(actions.deletedPeer.failure, (store, action) => action.payload) .handleAction(actions.setDeletePeer, (store, action) => action.payload) + .handleAction(actions.resetDeletedPeer, () => initialState.deletedPeer) + +const updateGroupsVisible = createReducer(initialState.setUpdateGroupsVisible) + .handleAction(actions.setUpdateGroupsVisible, (store, action) => action.payload) + +const savedGroups = createReducer, ActionTypes>(initialState.savedGroups) + .handleAction(actions.saveGroups.request, () => initialState.savedGroups) + .handleAction(actions.saveGroups.success, (store, action) => action.payload) + .handleAction(actions.saveGroups.failure, (store, action) => action.payload) + .handleAction(actions.resetSavedGroups, () => initialState.savedGroups) export default combineReducers({ data, @@ -62,5 +83,7 @@ export default combineReducers({ loading, failed, saving, - deletedPeer + deletedPeer, + updateGroupsVisible, + savedGroups }); diff --git a/src/store/peer/sagas.ts b/src/store/peer/sagas.ts index 72d5b5b..9c87d07 100644 --- a/src/store/peer/sagas.ts +++ b/src/store/peer/sagas.ts @@ -1,8 +1,19 @@ -import {all, call, put, select, takeLatest} from 'redux-saga/effects'; -import {ApiError, ApiResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types'; +import {all, call, spawn, put, select, takeLatest} from 'redux-saga/effects'; +import { + ApiError, + ApiResponse, + ChangeResponse, + CreateResponse, + DeleteResponse, + RequestPayload +} from '../../services/api-client/types'; import { Peer } from './types' import service from './service'; import actions from './actions'; +import {Group, GroupPeer} from "../group/types"; +import serviceGroup from "../group/service"; +import {actions as groupActions} from "../group"; + export function* getPeers(action: ReturnType): Generator { try { @@ -17,7 +28,6 @@ export function* getPeers(action: ReturnType): const effect = yield call(service.getPeers, action.payload); const response = effect as ApiResponse; - yield put(actions.getPeers.success(response.body)); } catch (err) { yield put(actions.getPeers.failure(err as ApiError)); @@ -50,7 +60,7 @@ export function* deletePeer(action: ReturnType)); 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.ip !== action.payload.payload))) } catch (err) { yield put(actions.deletedPeer.failure({ loading: false, @@ -62,10 +72,92 @@ export function* deletePeer(action: ReturnType): Generator { + try { + yield put(actions.setSavedGroups({ + loading: true, + success: false, + failure: false, + error: null, + data: null + })) + + const currentGroups = [...(yield select(state => state.group.data)) as Group[]] + + const peerGroupsToSave = action.payload.payload + + let groupsToSave = [] as Group[] + let groupsNoId = [] as Group[] + + groupsToSave = groupsToSave.concat( + currentGroups + .filter(g => peerGroupsToSave.groupsToRemove.includes(g.id || '')) + .map(g => ({ + id: g.id, + name: g.name, + peers: (g.peers as GroupPeer[]).filter(p => p.id !== peerGroupsToSave.ID).map(p => p.id) as string[] + })) + ) + + groupsToSave = groupsToSave.concat( + currentGroups + .filter(g => peerGroupsToSave.groupsToAdd.includes(g.id || '')) + .map(g => ({ + id: g.id, + name: g.name, + Peers: g.peers ? [...(g.peers as GroupPeer[]).map((p:GroupPeer) => p.id), peerGroupsToSave.ID] : [peerGroupsToSave.ID] + })) + ) + + groupsNoId = peerGroupsToSave.groupsNoId.map(g => ({ + name: g, + peers: [peerGroupsToSave.ID] + })) + + if (!groupsNoId.length && !groupsToSave.length) { + return + } + + const responsesGroup = yield all(groupsToSave.map(g => call(serviceGroup.editGroup, { + getAccessTokenSilently: action.payload.getAccessTokenSilently, + payload: g + }) + )) + + const responsesGroupNoId = yield all(groupsNoId.map(g => call(serviceGroup.createGroup, { + getAccessTokenSilently: action.payload.getAccessTokenSilently, + payload: g + }) + )) + + yield put(actions.saveGroups.success({ + loading: false, + success: true, + failure: false, + error: null, + data: [...(responsesGroup as ApiResponse[]).map(r => r.body), ...(responsesGroupNoId as ApiResponse[]).map(r => r.body)] + } as CreateResponse)) + + yield put(groupActions.getGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null })); + yield put(actions.getPeers.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null })); + + } catch (err) { + console.log(err) + yield put(actions.saveGroups.failure({ + loading: false, + success: false, + failure: true, + error: err as ApiError, + data: null + } as ChangeResponse)); + } +} + export default function* sagas(): Generator { yield all([ takeLatest(actions.getPeers.request, getPeers), - takeLatest(actions.deletedPeer.request, deletePeer) + takeLatest(actions.deletedPeer.request, deletePeer), + takeLatest(actions.saveGroups.request, saveGroups) ]); } diff --git a/src/store/peer/types.ts b/src/store/peer/types.ts index c9ef566..b6fcf8c 100644 --- a/src/store/peer/types.ts +++ b/src/store/peer/types.ts @@ -1,9 +1,23 @@ +import {Group} from "../group/types"; + export interface Peer { - Name: string, - IP: string, - Connected: boolean, - LastSeen: string, - OS: string, - Version: string, - Groups?: any[] + id?: string, + name: string, + ip: string, + connected: boolean, + last_seen: string, + os: string, + version: string, + groups?: Group[] +} + +export interface PeerToSave extends Peer { + groupsToSave: string[] +} + +export interface PeerGroupsToSave { + ID: string; + groupsToRemove: string[]; + groupsToAdd: string[]; + groupsNoId: string[]; } \ No newline at end of file diff --git a/src/store/rule/actions.ts b/src/store/rule/actions.ts index 69e0910..14ce000 100644 --- a/src/store/rule/actions.ts +++ b/src/store/rule/actions.ts @@ -15,6 +15,7 @@ const actions = { 'SAVE_RULE_FAILURE', ), CreateResponse, CreateResponse>(), setSavedRule: createAction('SET_CREATE_RULE')>(), + resetSavedRule: createAction('RESET_CREATE_RULE')(), deleteRule: createAsyncAction( 'DELETE_RULE_REQUEST', @@ -22,7 +23,9 @@ const actions = { 'DELETE_RULE_FAILURE' ), DeleteResponse, DeleteResponse>(), setDeletedRule: createAction('SET_DELETED_RULE')>(), + resetDeletedRule: createAction('RESET_DELETED_RULE')(), removeRule: createAction('REMOVE_RULE')(), + setRule: createAction('SET_RULE')(), setSetupNewRuleVisible: createAction('SET_SETUP_NEW_RULE_VISIBLE')() }; diff --git a/src/store/rule/reducer.ts b/src/store/rule/reducer.ts index e5784aa..dfbacee 100644 --- a/src/store/rule/reducer.ts +++ b/src/store/rule/reducer.ts @@ -64,13 +64,15 @@ const deletedRule = createReducer, ActionTypes>(in .handleAction(actions.deleteRule.request, () => initialState.deleteRule) .handleAction(actions.deleteRule.success, (store, action) => action.payload) .handleAction(actions.deleteRule.failure, (store, action) => action.payload) - .handleAction(actions.setDeletedRule, (store, action) => action.payload); + .handleAction(actions.setDeletedRule, (store, action) => action.payload) + .handleAction(actions.resetDeletedRule, () => initialState.deleteRule) const savedRule = createReducer, ActionTypes>(initialState.savedRule) .handleAction(actions.saveRule.request, () => initialState.savedRule) .handleAction(actions.saveRule.success, (store, action) => action.payload) .handleAction(actions.saveRule.failure, (store, action) => action.payload) .handleAction(actions.setSavedRule, (store, action) => action.payload) + .handleAction(actions.resetSavedRule, () => initialState.savedRule) const setupNewRuleVisible = createReducer(initialState.setupNewRuleVisible) .handleAction(actions.setSetupNewRuleVisible, (store, action) => action.payload) diff --git a/src/store/rule/sagas.ts b/src/store/rule/sagas.ts index d87c970..08a33a3 100644 --- a/src/store/rule/sagas.ts +++ b/src/store/rule/sagas.ts @@ -32,7 +32,7 @@ export function* setCreatedRule(action: ReturnType } function getNewGroupIds(dataString:string[], responses:Group[]):string[] { - return responses.filter(r => dataString.includes(r.Name)).map(r => r.ID || '') + return responses.filter(r => dataString.includes(r.name)).map(r => r.id || '') } export function* saveRule(action: ReturnType): Generator { @@ -49,7 +49,7 @@ export function* saveRule(action: ReturnType): const responsesGroup = yield all(ruleToSave.groupsToSave.map(g => call(serviceGroup.createGroup, { getAccessTokenSilently: action.payload.getAccessTokenSilently, - payload: { Name: g } + payload: { name: g } }) )) @@ -60,27 +60,26 @@ export function* saveRule(action: ReturnType): const newGroups = [...currentGroups, ...resGroups] yield put(groupActions.getGroups.success(newGroups)); - console.log(resGroups) - console.log(ruleToSave.groupsToSave) const newSources = getNewGroupIds(ruleToSave.sourcesNoId, resGroups) const newDestinations = getNewGroupIds(ruleToSave.destinationsNoId, resGroups) - console.log(newDestinations) const payloadToSave = { getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: { - Name: ruleToSave.Name, - Source: [...ruleToSave.Source as string[], ...newSources], - Destination: [...ruleToSave.Destination as string[], ...newDestinations], - Flow: ruleToSave.Flow + name: ruleToSave.name, + description: ruleToSave.description, + sources: [...ruleToSave.sources as string[], ...newSources], + destinations: [...ruleToSave.destinations as string[], ...newDestinations], + flow: ruleToSave.flow, + disabled: ruleToSave.disabled } as Rule } let effect - if (!ruleToSave.ID) { + if (!ruleToSave.id) { effect = yield call(service.createRule, payloadToSave); } else { - payloadToSave.payload.ID = ruleToSave.ID + payloadToSave.payload.id = ruleToSave.id effect = yield call(service.editRule, payloadToSave); } @@ -133,7 +132,7 @@ export function* deleteRule(action: ReturnType)); const rules = (yield select(state => state.rule.data)) as Rule[] - yield put(actions.getRules.success(rules.filter((p:Rule) => p.ID !== action.payload.payload))) + yield put(actions.getRules.success(rules.filter((p:Rule) => p.id !== action.payload.payload))) } catch (err) { yield put(actions.deleteRule.failure({ loading: false, diff --git a/src/store/rule/service.ts b/src/store/rule/service.ts index 0c8a267..31eed9c 100644 --- a/src/store/rule/service.ts +++ b/src/store/rule/service.ts @@ -23,8 +23,10 @@ export default { ); }, async editRule(payload:RequestPayload): Promise> { + const id = payload.payload.id + delete payload.payload.id return apiClient.put( - `/api/rules`, + `/api/rules/${id}`, payload ); }, diff --git a/src/store/rule/types.ts b/src/store/rule/types.ts index f50938f..3f0b90c 100644 --- a/src/store/rule/types.ts +++ b/src/store/rule/types.ts @@ -1,11 +1,13 @@ import {Group} from "../group/types"; export interface Rule { - ID?: string - Name: string - Source: Group[] | string[] | null - Destination: Group[] | string[] | null - Flow: string + id?: string + name: string + description: string + sources: Group[] | string[] | null + destinations: Group[] | string[] | null + flow: string + disabled: boolean } export interface RuleToSave extends Rule { diff --git a/src/store/setup-key/actions.ts b/src/store/setup-key/actions.ts index 8208093..1caedf0 100644 --- a/src/store/setup-key/actions.ts +++ b/src/store/setup-key/actions.ts @@ -28,13 +28,16 @@ const actions = { 'DELETE_SETUP_KEY_FAILURE' ), DeleteResponse, DeleteResponse>(), setDeleteSetupKey: createAction('SET_DELETE_SETUP_KEY')>(), + resetDeletedSetupKey: createAction('RESET_DELETE_SETUP_KEY')(), revokeSetupKey: createAsyncAction( 'REVOKE_SETUP_KEY_REQUEST', 'REVOKE_SETUP_KEY_SUCCESS', 'REVOKE_SETUP_KEY_FAILURE' ), ChangeResponse, ChangeResponse>(), - setRevokeSetupKey: createAction('SET_REVOKE_SETUP_KEY')>(), + setRevokeSetupKey: createAction('SET_REVOKED_SETUP_KEY')>(), + resetRevokedSetupKey: createAction('RESET_REVOKED_SETUP_KEY')(), + removeSetupKey: createAction('REMOVE_SETUP_KEY')(), setSetupKey: createAction('SET_SETUP_KEY')(), diff --git a/src/store/setup-key/reducer.ts b/src/store/setup-key/reducer.ts index cf9b9ae..a45f03f 100644 --- a/src/store/setup-key/reducer.ts +++ b/src/store/setup-key/reducer.ts @@ -72,13 +72,15 @@ const deletedSetupKey = createReducer, ActionTypes .handleAction(actions.deleteSetupKey.request, () => initialState.deletedSetupKey) .handleAction(actions.deleteSetupKey.success, (store, action) => action.payload) .handleAction(actions.deleteSetupKey.failure, (store, action) => action.payload) - .handleAction(actions.setDeleteSetupKey, (store, action) => action.payload); + .handleAction(actions.setDeleteSetupKey, (store, action) => action.payload) + .handleAction(actions.resetDeletedSetupKey, (store, action) => initialState.deletedSetupKey); const revokedSetupKey = createReducer, ActionTypes>(initialState.revokedSetupKey) .handleAction(actions.revokeSetupKey.request, () => initialState.revokedSetupKey) .handleAction(actions.revokeSetupKey.success, (store, action) => action.payload) .handleAction(actions.revokeSetupKey.failure, (store, action) => action.payload) .handleAction(actions.setRevokeSetupKey, (store, action) => action.payload) + .handleAction(actions.resetRevokedSetupKey, () => initialState.revokedSetupKey) const createdSetupKey = createReducer, ActionTypes>(initialState.createdSetupKey) .handleAction(actions.createSetupKey.request, () => initialState.createdSetupKey) diff --git a/src/store/setup-key/sagas.ts b/src/store/setup-key/sagas.ts index 5653881..0086b1e 100644 --- a/src/store/setup-key/sagas.ts +++ b/src/store/setup-key/sagas.ts @@ -3,18 +3,9 @@ import {ApiError, ApiResponse, ChangeResponse, CreateResponse, DeleteResponse} f import {SetupKey, SetupKeyRevoke} from './types' import service from './service'; import actions from './actions'; -import {take} from "lodash"; export function* getSetupKeys(action: ReturnType): Generator { try { - yield put(actions.setDeleteSetupKey({ - loading: false, - success: false, - failure: false, - error: null, - data: null - } as DeleteResponse)) - const effect = yield call(service.getSetupKeys, action.payload); const response = effect as ApiResponse; @@ -89,7 +80,7 @@ export function* deleteSetupKey(action: ReturnType)); const setupKeys = (yield select(state => state.setupKey.data)) as SetupKey[] - yield put(actions.getSetupKeys.success(setupKeys.filter((p:SetupKey) => p.Id !== action.payload.payload))) + yield put(actions.getSetupKeys.success(setupKeys.filter((p:SetupKey) => p.id !== action.payload.payload))) } catch (err) { yield put(actions.deleteSetupKey.failure({ loading: false, @@ -123,12 +114,12 @@ export function* revokeSetupKey(action: ReturnType)); const setupKeys = [...(yield select(state => state.setupKey.data)) as SetupKey[]] - let setupKey = setupKeys.find(s => s.Id === response.body.Id) as SetupKey + let setupKey = setupKeys.find(s => s.id === response.body.id) as SetupKey if (setupKey) { - setupKey.Revoked = response.body.Revoked - setupKey.Valid = response.body.Valid - setupKey.State = response.body.State - setupKey.Expires = response.body.Expires + setupKey.revoked = response.body.revoked + setupKey.valid = response.body.valid + setupKey.state = response.body.state + setupKey.expires = response.body.expires } yield put(actions.getSetupKeys.success(setupKeys)); } catch (err) { diff --git a/src/store/setup-key/service.ts b/src/store/setup-key/service.ts index 756c31c..6b40715 100644 --- a/src/store/setup-key/service.ts +++ b/src/store/setup-key/service.ts @@ -17,13 +17,13 @@ export default { }, async revokeSetupKey(payload:RequestPayload): Promise> { return apiClient.put( - `/api/setup-keys/` + payload.payload.Id, + `/api/setup-keys/` + payload.payload.id, payload ); }, async renameSetupKey(payload:RequestPayload): Promise> { return apiClient.put( - `/api/setup-keys/` + payload.payload.Id, + `/api/setup-keys/` + payload.payload.id, payload ); }, diff --git a/src/store/setup-key/types.ts b/src/store/setup-key/types.ts index d45bd3c..29eb320 100644 --- a/src/store/setup-key/types.ts +++ b/src/store/setup-key/types.ts @@ -1,23 +1,23 @@ export interface SetupKey { - Expires: string; - Id: string; - Key: string; - LastUsed: string; - Name: string; - Revoked: boolean; - State: string; - Type: string; - UsedTimes: number; - Valid: boolean; + expires: string; + id: string; + key: string; + last_used: string; + name: string; + revoked: boolean; + state: string; + type: string; + used_times: number; + valid: boolean; } export interface SetupKeyNew { - Id: string; - Name: string; - Type: string; + id: string; + name: string; + type: string; } export interface SetupKeyRevoke { - Id: string; - Revoked: boolean; + id: string; + revoked: boolean; } diff --git a/src/views/AccessControl.tsx b/src/views/AccessControl.tsx index 1f7d393..84d2ed4 100644 --- a/src/views/AccessControl.tsx +++ b/src/views/AccessControl.tsx @@ -4,7 +4,7 @@ import { Alert, Button, Card, Col, Dropdown, Input, Menu, message, Modal, Popover, Radio, RadioChangeEvent, - Row, Select, Space, Table, Tag, + Row, Select, Space, Table, Tag, Tooltip, Typography } from "antd"; import {Container} from "../components/Container"; @@ -24,6 +24,8 @@ 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; const { Column } = Table; @@ -55,7 +57,7 @@ export const AccessControl = () => { const [showTutorial, setShowTutorial] = useState(true) const [textToSearch, setTextToSearch] = useState(''); - const [optionAllEnable, setOptionAllEnable] = useState('all'); + const [optionAllEnable, setOptionAllEnable] = useState('enabled'); const [pageSize, setPageSize] = useState(5); const [currentPage, setCurrentPage] = useState(1); const [dataTable, setDataTable] = useState([] as RuleDataTable[]); @@ -68,7 +70,7 @@ export const AccessControl = () => { {label: "15", value: "15"} ] - const optionsAllEnabled = [{label: 'All', value: 'all'}, {label: 'Enabled', value: 'enabled'}] + const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'},{label: 'All', value: 'all'}] const itemsMenuAction = [ { @@ -87,22 +89,22 @@ export const AccessControl = () => { const actionsMenu = () const getSourceDestinationLabel = (data:Group[]):string => { - return (!data) ? "No group" : (data.length > 1) ? `${data.length} Groups` : (data.length === 1) ? data[0].Name : "No group" + return (!data) ? "No group" : (data.length > 1) ? `${data.length} Groups` : (data.length === 1) ? data[0].name : "No group" } const isShowTutorial = (rules:Rule[]):boolean => { - return (!rules.length || (rules.length === 1 && rules[0].Name === "Default")) + return (!rules.length || (rules.length === 1 && rules[0].name === "Default")) } const transformDataTable = (d:Rule[]):RuleDataTable[] => { return d.map(p => { - const sourceLabel = getSourceDestinationLabel(p.Source as Group[]) - const destinationLabel = getSourceDestinationLabel(p.Destination as Group[]) + const sourceLabel = getSourceDestinationLabel(p.sources as Group[]) + const destinationLabel = getSourceDestinationLabel(p.destinations as Group[]) return { - key: p.ID, ...p, - sourceCount: p.Source?.length, + key: p.id, ...p, + sourceCount: p.sources?.length, sourceLabel, - destinationCount: p.Destination?.length, + destinationCount: p.destinations?.length, destinationLabel } as RuleDataTable }) @@ -115,7 +117,7 @@ export const AccessControl = () => { useEffect(() => { setShowTutorial(isShowTutorial(rules)) - setDataTable(sortBy(transformDataTable(rules), "Name")) + setDataTable(sortBy(transformDataTable(filterDataTable()), "name")) }, [rules]) useEffect(() => { @@ -127,14 +129,16 @@ export const AccessControl = () => { const saveKey = 'saving'; useEffect(() => { if (savedRule.loading) { - message.loading({ content: 'Saving...', key: saveKey, duration: 0, style: styleNotification }); + message.loading({ content: 'Saving...', key: saveKey, duration: 0, style: styleNotification }) } else if (savedRule.success) { - message.success({ content: 'Rule saved with success!', key: saveKey, duration: 2, style: styleNotification }); - dispatch(ruleActions.setSetupNewRuleVisible(false)); - dispatch(ruleActions.setSavedRule({ ...savedRule, success: false })); + message.success({ content: 'Rule has been successfully updated.', key: saveKey, duration: 2, style: styleNotification }); + dispatch(ruleActions.setSetupNewRuleVisible(false)) + dispatch(ruleActions.setSavedRule({ ...savedRule, success: false })) + dispatch(ruleActions.resetSavedRule(null)) } else if (savedRule.error) { - message.error({ content: 'Error! Something wrong to create key.', key: saveKey, duration: 2, style: styleNotification }); - dispatch(ruleActions.setSavedRule({ ...savedRule, error: null })); + message.error({ content: 'Failed to update rule. You might not have enough permissions.', key: saveKey, duration: 2, style: styleNotification }); + dispatch(ruleActions.setSavedRule({ ...savedRule, error: null })) + dispatch(ruleActions.resetSavedRule(null)) } }, [savedRule]) @@ -142,11 +146,13 @@ export const AccessControl = () => { useEffect(() => { const style = { marginTop: 85 } if (deletedRule.loading) { - message.loading({ content: 'Deleting...', key: deleteKey, style }); + message.loading({ content: 'Deleting...', key: deleteKey, style }) } else if (deletedRule.success) { - message.success({ content: 'Rule deleted with success!', key: deleteKey, duration: 2, style }); + message.success({ content: 'Rule has been successfully disabled.', key: deleteKey, duration: 2, style }) + dispatch(ruleActions.resetDeletedRule(null)) } else if (deletedRule.error) { - message.error({ content: 'Error! Something wrong to delete rule.', key: deleteKey, duration: 2, style }); + message.error({ content: 'Failed to remove rule. You might not have enough permissions.', key: deleteKey, duration: 2, style }) + dispatch(ruleActions.resetDeletedRule(null)) } }, [deletedRule]) @@ -174,14 +180,14 @@ export const AccessControl = () => { content: {ruleToAction && <> - Delete rule "{ruleToAction ? ruleToAction.Name : ''}" + Delete rule "{ruleToAction ? ruleToAction.name : ''}" Are you sure you want to delete peer from your account? } , okType: 'danger', onOk() { - dispatch(ruleActions.deleteRule.request({getAccessTokenSilently, payload: ruleToAction?.ID || ''})); + dispatch(ruleActions.deleteRule.request({getAccessTokenSilently, payload: ruleToAction?.id || ''})); }, onCancel() { setRuleToAction(null); @@ -196,14 +202,14 @@ export const AccessControl = () => { content: {ruleToAction && <> - Deactivate rule "{ruleToAction ? ruleToAction.Name : ''}" + Deactivate rule "{ruleToAction ? ruleToAction.name : ''}" Are you sure you want to deactivate peer from your account? } , okType: 'danger', onOk() { - //dispatch(ruleActions.deleteRule.request({getAccessTokenSilently, payload: ruleToAction?.ID || ''})); + //dispatch(ruleActions.deleteRule.request({getAccessTokenSilently, payload: ruleToAction?.id || ''})); }, onCancel() { setRuleToAction(null); @@ -214,32 +220,49 @@ export const AccessControl = () => { const filterDataTable = ():Rule[] => { const t = textToSearch.toLowerCase().trim() let f:Rule[] = filter(rules, (f:Rule) => - (f.Name.toLowerCase().includes(t) || t === "") + (f.name.toLowerCase().includes(t) || f.description.toLowerCase().includes(t) || t === "") ) as Rule[] - // if (optionAllEnabled === "enabled") { - // f = filter(rules, (f:Rule) => f.) - // } + if (optionAllEnable !== "all") { + f = filter(f, (f:Rule) => !f.disabled) + } return f } const onClickAddNewRule = () => { dispatch(ruleActions.setSetupNewRuleVisible(true)); dispatch(ruleActions.setRule({ - Name: '', - Source: [], - Destination: [], - Flow: 'bidirect' + name: '', + description: '', + sources: [], + destinations: [], + flow: 'bidirect', + disabled: false } as Rule)) } const onClickViewRule = () => { dispatch(ruleActions.setSetupNewRuleVisible(true)); dispatch(ruleActions.setRule({ - ID: ruleToAction?.ID || null, - Name: ruleToAction?.Name, - Source: ruleToAction?.Source, - Destination: ruleToAction?.Destination, - Flow: ruleToAction?.Flow + id: ruleToAction?.id || null, + name: ruleToAction?.name, + description: ruleToAction?.description, + sources: ruleToAction?.sources, + destinations: ruleToAction?.destinations, + flow: ruleToAction?.flow, + disabled: ruleToAction?.disabled + } as Rule)) + } + + const setRuleAndView = (rule: RuleDataTable) => { + dispatch(ruleActions.setSetupNewRuleVisible(true)); + dispatch(ruleActions.setRule({ + id: rule.id || null, + name: rule.name, + description: rule.description, + sources: rule.sources, + destinations: rule.destinations, + flow: rule.flow, + disabled: rule.disabled } as Rule)) } @@ -251,17 +274,17 @@ export const AccessControl = () => { }) } - const renderPopoverGroups = (label: string, groups:Group[] | string[] | null) => { - const content = groups?.map(g => { + const renderPopoverGroups = (label: string, groups:Group[] | string[] | null, rule: RuleDataTable) => { + const content = groups?.map((g, i) => { const _g = g as Group - const peersCount = ` - ${_g.PeersCount || 0} ${(_g.PeersCount && parseInt(_g.PeersCount) > 1) ? 'peers' : 'peer'} ` + const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} ` return ( -
+
- {_g.Name} + {_g.name} {peersCount}
@@ -269,7 +292,7 @@ export const AccessControl = () => { }) return ( {content}} title={null}> - + ) } @@ -280,7 +303,7 @@ export const AccessControl = () => { Access Control - Create and control access groups + Access rules help you manage access permissions in your organisation. @@ -288,13 +311,13 @@ export const AccessControl = () => { - {/**/} + /> - - - - + + + + Peers + A list of all the machines in your account including their name, IP and status. + + + + {/**/} + + + + + + - - - - - - Add Peer - - - - - {failed && - - } - {loading && } - - `Showing ${range[0]} to ${range[1]} of ${total} peers`)}} - className="card-table" - showSorterTooltip={false} - scroll={{x: true}} - dataSource={dataTable}> - (record as any).Name.includes(value)} - sorter={(a, b) => ((a as any).Name.localeCompare((b as any).Name))} /> - { - const _a = (a as any).IP.split('.') - const _b = (b as any).IP.split('.') - const a_s = _a.map((i:any) => i.padStart(3, '0')).join() - const b_s = _b.map((i:any) => i.padStart(3, '0')).join() - return a_s.localeCompare(b_s) - }} - render={(text, record, index) => { - return - }} - /> - { - return text ? online : offline - }} - /> - { - return (record as PeerDataTable).Connected ? 'just now' : timeAgo(text) - }} - /> - { - return formatOS(text) - }} - /> - - { - return { - if (visible) setPeerToAction(record as PeerDataTable) - }}> - }} - /> -
-
-
- -
-
+ { + return text ? online : offline + }} + /> + { + return renderPopoverGroups(text, record.groups, record) + }} + /> + { + return (record as PeerDataTable).connected ? 'just now' : timeAgo(text) + }} + /> + { + return formatOS(text) + }} + /> + + { + return { + if (visible) setPeerToAction(record as PeerDataTable) + }}> + }} + /> + + +
+ +
+ + + ) } diff --git a/src/views/SetupKeys.tsx b/src/views/SetupKeys.tsx index 59cb9cb..e608c28 100644 --- a/src/views/SetupKeys.tsx +++ b/src/views/SetupKeys.tsx @@ -26,6 +26,8 @@ import {copyToClipboard, formatDate, formatOS, timeAgo} from "../utils/common"; import {ExclamationCircleOutlined} from "@ant-design/icons"; import SetupKeyNew from "../components/SetupKeyNew"; import ButtonCopyMessage from "../components/ButtonCopyMessage"; +import TableSpin from "../components/Spin"; +import tableSpin from "../components/Spin"; const { Title, Text, Paragraph } = Typography; const { Column } = Table; @@ -75,7 +77,7 @@ export const SetupKeys = () => { const actionsMenu = () const transformDataTable = (d:SetupKey[]):SetupKeyDataTable[] => { - return d.map(p => ({ key: p.Id, ...p } as SetupKeyDataTable)) + return d.map(p => ({ ...p } as SetupKeyDataTable)) } useEffect(() => { @@ -96,23 +98,25 @@ export const SetupKeys = () => { message.loading({ content: 'Deleting...', key: deleteKey, style: styleNotification }); } else if (deletedSetupKey.success) { message.success({ content: 'Setup key has been successfully removed.', key: deleteKey, duration: 2, style: styleNotification }); - dispatch(setupKeyActions.setDeleteSetupKey({ ...deletedSetupKey, success: false })); + dispatch(setupKeyActions.setDeleteSetupKey({ ...deletedSetupKey, success: false })) + dispatch(setupKeyActions.resetDeletedSetupKey(null)) } else if (deletedSetupKey.error) { message.error({ content: 'Failed to delete setup key. You might not have enough permissions.', key: deleteKey, duration: 2, style: styleNotification }); - dispatch(setupKeyActions.setDeleteSetupKey({ ...deletedSetupKey, error: null })); + dispatch(setupKeyActions.setDeleteSetupKey({ ...deletedSetupKey, error: null })) + dispatch(setupKeyActions.resetDeletedSetupKey(null)) } }, [deletedSetupKey]) - const revokeKey = 'creating'; + const revokeKey = 'revoking'; useEffect(() => { if (revokedSetupKey.loading) { - message.loading({ content: 'Revoking...', key: revokeKey, duration: 0, style: styleNotification }); + message.loading({ content: 'Revoking...', key: revokeKey, duration: 0, style: styleNotification }) } else if (revokedSetupKey.success) { message.success({ content: 'Setup key has been successfully revoked.', key: revokeKey, duration: 2, style: styleNotification }); - dispatch(setupKeyActions.setRevokeSetupKey({ ...revokedSetupKey, success: false })); + dispatch(setupKeyActions.resetRevokedSetupKey(null)) } else if (revokedSetupKey.error) { message.error({ content: 'Failed to revoke setup key. You might not have enough permissions.', key: revokeKey, duration: 2, style: styleNotification }); - dispatch(setupKeyActions.setRevokeSetupKey({ ...revokedSetupKey, error: null })); + dispatch(setupKeyActions.resetRevokedSetupKey(null)) } }, [revokedSetupKey]) @@ -134,10 +138,10 @@ export const SetupKeys = () => { const t = textToSearch.toLowerCase().trim() let f:SetupKey[] = [...setupKeys] if (optionValidAll === "valid") { - f = filter(setupKeys, (_f:SetupKey) => _f.Valid && !_f.Revoked) + f = filter(setupKeys, (_f:SetupKey) => _f.valid && !_f.revoked) } f = filter(f, (_f:SetupKey) => - (_f.Name.toLowerCase().includes(t) || _f.State.includes(t) || _f.Type.includes(t) || _f.Key.includes(t) || t === "") + (_f.name.toLowerCase().includes(t) || _f.state.includes(t) || _f.type.toLowerCase().includes(t) || _f.key.toLowerCase().includes(t) || t === "") ) as SetupKey[] return f } @@ -166,14 +170,14 @@ export const SetupKeys = () => { content: {setupKeyToAction && <> - Delete setupKey "{setupKeyToAction ? setupKeyToAction.Name : ''}" + Delete setupKey "{setupKeyToAction ? setupKeyToAction.name : ''}" Are you sure you want to delete key? } , okType: 'danger', onOk() { - dispatch(setupKeyActions.deleteSetupKey.request({getAccessTokenSilently, payload: setupKeyToAction ? setupKeyToAction.Id : ''})); + dispatch(setupKeyActions.deleteSetupKey.request({getAccessTokenSilently, payload: setupKeyToAction ? setupKeyToAction.id : ''})); }, onCancel() { setSetupKeyToAction(null); @@ -188,14 +192,14 @@ export const SetupKeys = () => { content: {setupKeyToAction && <> - Revoke setupKey "{setupKeyToAction ? setupKeyToAction.Name : ''}" + Revoke setupKey "{setupKeyToAction ? setupKeyToAction.name : ''}" Are you sure you want to revoke key? } , okType: 'danger', onOk() { - dispatch(setupKeyActions.revokeSetupKey.request({getAccessTokenSilently, payload: { Id: setupKeyToAction ? setupKeyToAction.Id : null, Revoked: true } as SetupKeyRevoke})); + dispatch(setupKeyActions.revokeSetupKey.request({getAccessTokenSilently, payload: { id: setupKeyToAction ? setupKeyToAction.id : null,revoked: true } as SetupKeyRevoke})); }, onCancel() { setSetupKeyToAction(null); @@ -206,8 +210,8 @@ export const SetupKeys = () => { const onClickAddNewSetupKey = () => { dispatch(setupKeyActions.setSetupNewKeyVisible(true)); dispatch(setupKeyActions.setSetupKey({ - Name: '', - Type: 'reusable' + name: '', + type: 'reusable' } as SetupKey)) } @@ -252,49 +256,51 @@ export const SetupKeys = () => { {failed && } - {loading && } `Showing ${range[0]} to ${range[1]} of ${total} setup keys`)}} className="card-table" showSorterTooltip={false} scroll={{x: true}} + loading={tableSpin(loading)} dataSource={dataTable}> - (record as any).Name.includes(value)} - sorter={(a, b) => ((a as any).Name.localeCompare((b as any).Name))} + (record as any).name.includes(value)} + sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))} + defaultSortOrder='ascend' /> - { return (text === 'valid') ? {text} : {text} }} - sorter={(a, b) => ((a as any).State.localeCompare((b as any).State))} + sorter={(a, b) => ((a as any).state.localeCompare((b as any).state))} /> - (record as any).Type.includes(value)} - sorter={(a, b) => ((a as any).Type.localeCompare((b as any).Type))} + (record as any).type.includes(value)} + sorter={(a, b) => ((a as any).type.localeCompare((b as any).type))} /> - (record as any).Key.includes(value)} - sorter={(a, b) => ((a as any).Key.localeCompare((b as any).Key))} + (record as any).key.includes(value)} + sorter={(a, b) => ((a as any).key.localeCompare((b as any).key))} render={(text, record, index) => { return }} /> - ((a as any).last_used.localeCompare((b as any).last_used))} render={(text, record, index) => { - return !(record as SetupKey).UsedTimes ? 'unused' : timeAgo(text) + return !(record as SetupKey).used_times ? 'unused' : timeAgo(text) }} /> - ((a as any).Type.localeCompare((b as any).Type))} + ((a as any).used_times - ((b as any).used_times))} /> - { return formatDate(text) }} @@ -302,7 +308,7 @@ export const SetupKeys = () => { { - return !(record as SetupKeyDataTable).Revoked ? ( + return !(record as SetupKeyDataTable).revoked ? ( { if (visible) setSetupKeyToAction(record as SetupKeyDataTable) diff --git a/src/views/Users.tsx b/src/views/Users.tsx index c5a86f4..43ab8d5 100644 --- a/src/views/Users.tsx +++ b/src/views/Users.tsx @@ -16,6 +16,7 @@ import { import { User } from "../store/user/types"; import {filter} from "lodash"; import {formatOS, timeAgo} from "../utils/common"; +import tableSpin from "../components/Spin"; const { Title, Paragraph } = Typography; const { Column } = Table; @@ -97,17 +98,18 @@ export const Activity = () => { {failed && } - {loading && }
`Showing ${range[0]} to ${range[1]} of ${total} users`)}} className="card-table" showSorterTooltip={false} scroll={{x: true}} + loading={tableSpin(loading)} dataSource={dataTable}> (record as any).email.includes(value)} sorter={(a, b) => ((a as any).email.localeCompare((b as any).email))} + defaultSortOrder='ascend' render={(text:string | null, record, index) => { return (text && text.trim() !== "") ? text : (record as User).id }}