mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
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 <raphael.oliveira@dataontabs.com> Co-authored-by: DataOnTabs <68952619+dataontabs@users.noreply.github.com> Co-authored-by: mlsmaycon <mlsmaycon@gmail.com>
This commit is contained in:
@@ -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() {
|
||||
<Route path='/peers' exact component={Peers}/>
|
||||
<Route path="/add-peer" component={AddPeer}/>
|
||||
<Route path="/setup-keys" component={SetupKeys}/>
|
||||
{/*<Route path="/acls" component={AccessControl}/>
|
||||
<Route path="/activity" component={Activity}/>*/}
|
||||
<Route path="/acls" component={AccessControl}/>
|
||||
{/*<Route path="/activity" component={Activity}/>*/}
|
||||
<Route path="/users" component={Users}/>
|
||||
</Switch>
|
||||
</Content>
|
||||
|
||||
@@ -27,9 +27,9 @@ const AccessControlModalGroups:React.FC<Props> = ({data, title, visible, onCance
|
||||
renderItem={(item:Group) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar>{item.Name.slice(0,1).toUpperCase()}</Avatar>}
|
||||
title={item.Name}
|
||||
description={`${item.PeersCount} peers`}
|
||||
avatar={<Avatar>{item.name.slice(0,1).toUpperCase()}</Avatar>}
|
||||
title={item.name}
|
||||
description={`${item.peers_count} peers`}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
|
||||
@@ -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<any>(null)
|
||||
const inputDescriptionRef = useRef<any>(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<HTMLSpanElement>) => {
|
||||
@@ -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 (
|
||||
<>
|
||||
<Tag
|
||||
@@ -164,10 +182,31 @@ const AccessControlNew = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z" fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
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={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedRule.loading}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedRule.loading} onClick={handleFormSubmit}>{`${formRule.ID ? 'Save' : 'Create'}`}</Button>
|
||||
<Button type="primary" disabled={savedRule.loading} onClick={handleFormSubmit}>{`${formRule.id ? 'Save' : 'Create'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
@@ -198,7 +238,7 @@ const AccessControlNew = () => {
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
<Row align="top">
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && formRule.ID &&
|
||||
{!editName && !editDescription && formRule.id &&
|
||||
<button type="button" aria-label="Close" className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
@@ -209,23 +249,61 @@ const AccessControlNew = () => {
|
||||
}
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
{ !editName && formRule.ID ? (
|
||||
<div className={"access-control ant-drawer-title"} onClick={() => toggleEditName(true)}>{formRule.ID ? formRule.Name : 'New Rule'}</div>
|
||||
{ !editName && formRule.id ? (
|
||||
<div className={"access-control input-text ant-drawer-title"} onClick={() => toggleEditName(true)}>{formRule.id ? formRule.name : 'New Rule'}</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="Name"
|
||||
label={null}
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{required: true, message: 'Please add a name for this access rule'}]}
|
||||
>
|
||||
<Input placeholder="Add rule name..." ref={inputNameRef} onPressEnter={() => toggleEditName(false)} onBlur={() => toggleEditName(false)} autoComplete="off"/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{ !editDescription ? (
|
||||
<div className={"access-control input-text ant-drawer-subtitle"} onClick={() => toggleEditDescription(true)}>{formRule.description && formRule.description.trim() !== "" ? formRule.description : 'Add description...'}</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{marginTop: 24}}
|
||||
>
|
||||
<Input placeholder="Add description..." ref={inputDescriptionRef} onPressEnter={() => toggleEditDescription(false)} onBlur={() => toggleEditDescription(false)} autoComplete="off"/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="top">
|
||||
<Col flex="auto">
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Header>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="disabled"
|
||||
label="Status"
|
||||
//valuePropName="checked"
|
||||
>
|
||||
{/*<Switch
|
||||
checkedChildren={<CheckOutlined />}
|
||||
unCheckedChildren={<CloseOutlined />}
|
||||
|
||||
onChange={handleChangeDisabled}
|
||||
/>*/}
|
||||
|
||||
<Radio.Group
|
||||
options={optionsDisabledEnabled}
|
||||
onChange={handleChangeDisabled}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
@@ -234,7 +312,13 @@ const AccessControlNew = () => {
|
||||
rules={[{required: true, message: 'Please enter ate least one group'}]}
|
||||
style={{display: 'flex'}}
|
||||
>
|
||||
<Select mode="tags" style={{ width: '100%' }} placeholder="Tags Mode" tagRender={tagRender} onChange={handleChangeSource}>
|
||||
<Select mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="Tags Mode"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeSource}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
@@ -250,7 +334,13 @@ const AccessControlNew = () => {
|
||||
rules={[{required: true, message: 'Please enter ate least one group'}]}
|
||||
style={{display: 'flex'}}
|
||||
>
|
||||
<Select mode="tags" style={{ width: '100%' }} placeholder="Tags Mode" tagRender={tagRender} onChange={handleChangeDestination}>
|
||||
<Select
|
||||
mode="tags" style={{ width: '100%' }}
|
||||
placeholder="Tags Mode"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeDestination}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
@@ -271,7 +361,6 @@ const AccessControlNew = () => {
|
||||
<Paragraph>
|
||||
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.
|
||||
</Paragraph>
|
||||
<a style={{color: 'rgb(07, 114, 128)'}} href="https://docs.netbird.io/overview/access-control" target="_blank">Learn more about access control...</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
@@ -279,7 +368,7 @@ const AccessControlNew = () => {
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://docs.netbird.io/docs/overview/acls" style={{color: 'rgb(07, 114, 128)'}}>Learn
|
||||
more about setup keys</Button>
|
||||
more about access controls</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
@@ -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: (<Link to="/peers">Peers</Link>), key: '/peers' },
|
||||
{ label: (<Link to="/add-peer">Add Peer</Link>), key: '/add-peer' },
|
||||
{ label: (<Link to="/setup-keys">Setup Keys</Link>), key: '/setup-keys' },
|
||||
/*{ label: (<Link to="/acls">Access Control</Link>), key: '/acls' },
|
||||
{ label: (<Link to="/activity">Activity</Link>), key: '/activity' },*/
|
||||
{ label: (<Link to="/acls">Access Control</Link>), key: '/acls' },
|
||||
// { label: (<Link to="/activity">Activity</Link>), key: '/activity' },
|
||||
{ label: (<Link to="/users">Users</Link>), key: '/users' }
|
||||
] as ItemType[])
|
||||
|
||||
@@ -80,7 +80,7 @@ const Navbar = () => {
|
||||
|
||||
const createAvatar = (size:AvatarSize) => {
|
||||
return user?.picture ? (
|
||||
<Avatar size={size} src={user?.picture}/>
|
||||
<Avatar size={size} src={user?.picture} icon={<UserOutlined />} />
|
||||
) : (
|
||||
<Avatar size={size}>{(user?.name || '').slice(0, 1).toUpperCase()}</Avatar>
|
||||
)
|
||||
|
||||
193
src/components/PeerGroupsUpdate.tsx
Normal file
193
src/components/PeerGroupsUpdate.tsx
Normal file
@@ -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<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color="blue"
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z" fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
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 &&
|
||||
<Drawer
|
||||
title={`${peer.name}`}
|
||||
forceRender={true}
|
||||
visible={true}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
autoFocus={true}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedGroups.loading}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedGroups.loading || (!peerGroupsToSave.groupsToRemove.length && !peerGroupsToSave.groupsToAdd.length && !peerGroupsToSave.groupsNoId.length)} onClick={handleFormSubmit}>Save</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label="Groups"
|
||||
rules={[{required: true, message: 'Please enter ate least one group'}]}
|
||||
style={{display: 'flex'}}
|
||||
>
|
||||
<Select mode="tags" style={{ width: '100%' }} placeholder="Select groups..." tagRender={tagRender} dropdownRender={dropDownRender} onChange={handleChangeTags}>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Drawer>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PeerGroupsUpdate
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
12
src/components/Spin.tsx
Normal file
12
src/components/Spin.tsx
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -25,19 +25,6 @@ const providerConfig = {
|
||||
onRedirectCallback,
|
||||
};
|
||||
|
||||
/*
|
||||
ReactDOM.render(
|
||||
<Auth0Provider {...providerConfig}>
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App/>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
</Auth0Provider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
*/
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
@@ -39,7 +39,7 @@ export function* saveGroup(action: ReturnType<typeof actions.saveGroup.request>)
|
||||
} as CreateResponse<Group | null>))
|
||||
|
||||
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<typeof actions.deleteGroup.reque
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
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,
|
||||
|
||||
@@ -29,8 +29,10 @@ export default {
|
||||
);
|
||||
},
|
||||
async editGroup(payload:RequestPayload<Group>): Promise<ApiResponse<Group>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<Group>(
|
||||
`${baseUrl}`,
|
||||
`${baseUrl}/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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',
|
||||
)<RequestPayload<null>, Peer[], ApiError>(),
|
||||
|
||||
deletedPeer: createAsyncAction(
|
||||
'DELETE_PEER_REQUEST',
|
||||
'DELETE_PEER_SUCCESS',
|
||||
'DELETE_PEER_FAILURE'
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
resetDeletedPeer: createAction('RESET_DELETED_PEER')<null>(),
|
||||
setDeletePeer: createAction('SET_DELETE_PEER')<DeleteResponse<string | null>>(),
|
||||
|
||||
saveGroups: createAsyncAction(
|
||||
'SAVE_PEERS_GROUPS_REQUEST',
|
||||
'SAVE_PEERS_GROUPS_SUCCESS',
|
||||
'SAVE_PEERS_GROUPS_FAILURE',
|
||||
)<RequestPayload<PeerGroupsToSave>, ChangeResponse<Group[] | null>, ChangeResponse<Group[] | null>>(),
|
||||
setSavedGroups: createAction('SET_SAVE_PEER_GROUPS')<ChangeResponse<Group[] | null>>(),
|
||||
resetSavedGroups: createAction('RESET_SAVE_PEER_GROUPS')<null>(),
|
||||
|
||||
removePeer: createAction('REMOVE_PEER')<string>(),
|
||||
setPeer: createAction('SET_PEER')<Peer>(),
|
||||
setPeer: createAction('SET_PEER')<Peer | null>(),
|
||||
setUpdateGroupsVisible: createAction('SET_UPDATE_GROUPS_VISIBLE')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
|
||||
@@ -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<string | null>;
|
||||
setUpdateGroupsVisible: boolean;
|
||||
savedGroups: ChangeResponse<Group[] | null>;
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
@@ -25,6 +28,14 @@ const initialState: StateType = {
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
setUpdateGroupsVisible: false,
|
||||
savedGroups: <ChangeResponse<Group[] | null>> {
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,7 +43,7 @@ const data = createReducer<Peer[], ActionTypes>(initialState.data as Peer[])
|
||||
.handleAction(actions.getPeers.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getPeers.failure, () => []);
|
||||
|
||||
const peer = createReducer<Peer, ActionTypes>(initialState.peer as Peer)
|
||||
const peer = createReducer<Peer | null, ActionTypes>(initialState.peer as Peer)
|
||||
.handleAction(actions.setPeer, (store, action) => action.payload);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
@@ -55,6 +66,16 @@ const deletedPeer = createReducer<DeleteResponse<string | null>, 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<boolean, ActionTypes>(initialState.setUpdateGroupsVisible)
|
||||
.handleAction(actions.setUpdateGroupsVisible, (store, action) => action.payload)
|
||||
|
||||
const savedGroups = createReducer<ChangeResponse<Group[] | null>, 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
|
||||
});
|
||||
|
||||
@@ -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<typeof actions.getPeers.request>): Generator {
|
||||
try {
|
||||
@@ -17,7 +28,6 @@ export function* getPeers(action: ReturnType<typeof actions.getPeers.request>):
|
||||
|
||||
const effect = yield call(service.getPeers, action.payload);
|
||||
const response = effect as ApiResponse<Peer[]>;
|
||||
|
||||
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<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.ip !== action.payload.payload)))
|
||||
} catch (err) {
|
||||
yield put(actions.deletedPeer.failure({
|
||||
loading: false,
|
||||
@@ -62,10 +72,92 @@ export function* deletePeer(action: ReturnType<typeof actions.deletedPeer.reques
|
||||
}
|
||||
}
|
||||
|
||||
export function* saveGroups(action: ReturnType<typeof actions.saveGroups.request>): 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<Group>[]).map(r => r.body), ...(responsesGroupNoId as ApiResponse<Group>[]).map(r => r.body)]
|
||||
} as CreateResponse<Group[] | null>))
|
||||
|
||||
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<Group[] | null>));
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -15,6 +15,7 @@ const actions = {
|
||||
'SAVE_RULE_FAILURE',
|
||||
)<RequestPayload<RuleToSave>, CreateResponse<Rule | null>, CreateResponse<Rule | null>>(),
|
||||
setSavedRule: createAction('SET_CREATE_RULE')<CreateResponse<Rule | null>>(),
|
||||
resetSavedRule: createAction('RESET_CREATE_RULE')<null>(),
|
||||
|
||||
deleteRule: createAsyncAction(
|
||||
'DELETE_RULE_REQUEST',
|
||||
@@ -22,7 +23,9 @@ const actions = {
|
||||
'DELETE_RULE_FAILURE'
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
setDeletedRule: createAction('SET_DELETED_RULE')<DeleteResponse<string | null>>(),
|
||||
resetDeletedRule: createAction('RESET_DELETED_RULE')<null>(),
|
||||
removeRule: createAction('REMOVE_RULE')<string>(),
|
||||
|
||||
setRule: createAction('SET_RULE')<Rule>(),
|
||||
setSetupNewRuleVisible: createAction('SET_SETUP_NEW_RULE_VISIBLE')<boolean>()
|
||||
};
|
||||
|
||||
@@ -64,13 +64,15 @@ const deletedRule = createReducer<DeleteResponse<string | null>, 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<CreateResponse<Rule | null>, 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<boolean, ActionTypes>(initialState.setupNewRuleVisible)
|
||||
.handleAction(actions.setSetupNewRuleVisible, (store, action) => action.payload)
|
||||
|
||||
@@ -32,7 +32,7 @@ export function* setCreatedRule(action: ReturnType<typeof actions.setSavedRule>
|
||||
}
|
||||
|
||||
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<typeof actions.saveRule.request>): Generator {
|
||||
@@ -49,7 +49,7 @@ export function* saveRule(action: ReturnType<typeof actions.saveRule.request>):
|
||||
|
||||
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<typeof actions.saveRule.request>):
|
||||
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<typeof actions.deleteRule.request
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
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,
|
||||
|
||||
@@ -23,8 +23,10 @@ export default {
|
||||
);
|
||||
},
|
||||
async editRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<Rule>(
|
||||
`/api/rules`,
|
||||
`/api/rules/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -28,13 +28,16 @@ const actions = {
|
||||
'DELETE_SETUP_KEY_FAILURE'
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
setDeleteSetupKey: createAction('SET_DELETE_SETUP_KEY')<DeleteResponse<string | null>>(),
|
||||
resetDeletedSetupKey: createAction('RESET_DELETE_SETUP_KEY')<null>(),
|
||||
|
||||
revokeSetupKey: createAsyncAction(
|
||||
'REVOKE_SETUP_KEY_REQUEST',
|
||||
'REVOKE_SETUP_KEY_SUCCESS',
|
||||
'REVOKE_SETUP_KEY_FAILURE'
|
||||
)<RequestPayload<SetupKeyRevoke>, ChangeResponse<SetupKey | null>, ChangeResponse<SetupKey | null>>(),
|
||||
setRevokeSetupKey: createAction('SET_REVOKE_SETUP_KEY')<ChangeResponse<SetupKey | null>>(),
|
||||
setRevokeSetupKey: createAction('SET_REVOKED_SETUP_KEY')<ChangeResponse<SetupKey | null>>(),
|
||||
resetRevokedSetupKey: createAction('RESET_REVOKED_SETUP_KEY')<null>(),
|
||||
|
||||
|
||||
removeSetupKey: createAction('REMOVE_SETUP_KEY')<string>(),
|
||||
setSetupKey: createAction('SET_SETUP_KEY')<SetupKey>(),
|
||||
|
||||
@@ -72,13 +72,15 @@ const deletedSetupKey = createReducer<DeleteResponse<string | null>, 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<ChangeResponse<SetupKey | null>, 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<CreateResponse<SetupKey | null>, ActionTypes>(initialState.createdSetupKey)
|
||||
.handleAction(actions.createSetupKey.request, () => initialState.createdSetupKey)
|
||||
|
||||
@@ -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<typeof actions.getSetupKeys.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setDeleteSetupKey({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>))
|
||||
|
||||
const effect = yield call(service.getSetupKeys, action.payload);
|
||||
const response = effect as ApiResponse<SetupKey[]>;
|
||||
|
||||
@@ -89,7 +80,7 @@ export function* deleteSetupKey(action: ReturnType<typeof actions.deleteSetupKey
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
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<typeof actions.revokeSetupKey
|
||||
} as ChangeResponse<SetupKey | null>));
|
||||
|
||||
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) {
|
||||
|
||||
@@ -17,13 +17,13 @@ export default {
|
||||
},
|
||||
async revokeSetupKey(payload:RequestPayload<SetupKeyRevoke>): Promise<ApiResponse<SetupKey>> {
|
||||
return apiClient.put<SetupKey>(
|
||||
`/api/setup-keys/` + payload.payload.Id,
|
||||
`/api/setup-keys/` + payload.payload.id,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async renameSetupKey(payload:RequestPayload<any>): Promise<ApiResponse<SetupKey>> {
|
||||
return apiClient.put<SetupKey>(
|
||||
`/api/setup-keys/` + payload.payload.Id,
|
||||
`/api/setup-keys/` + payload.payload.id,
|
||||
payload
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = (<Menu items={itemsMenuAction} ></Menu>)
|
||||
|
||||
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: <Space direction="vertical" size="small">
|
||||
{ruleToAction &&
|
||||
<>
|
||||
<Title level={5}>Delete rule "{ruleToAction ? ruleToAction.Name : ''}"</Title>
|
||||
<Title level={5}>Delete rule "{ruleToAction ? ruleToAction.name : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to delete peer from your account?</Paragraph>
|
||||
</>
|
||||
}
|
||||
</Space>,
|
||||
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: <Space direction="vertical" size="small">
|
||||
{ruleToAction &&
|
||||
<>
|
||||
<Title level={5}>Deactivate rule "{ruleToAction ? ruleToAction.Name : ''}"</Title>
|
||||
<Title level={5}>Deactivate rule "{ruleToAction ? ruleToAction.name : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to deactivate peer from your account?</Paragraph>
|
||||
</>
|
||||
}
|
||||
</Space>,
|
||||
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 (
|
||||
<div>
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
<strong>{_g.Name}</strong>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
@@ -269,7 +292,7 @@ export const AccessControl = () => {
|
||||
})
|
||||
return (
|
||||
<Popover content={<Space direction="vertical">{content}</Space>} title={null}>
|
||||
<Button type="link">{label}</Button>
|
||||
<Button type="link" onClick={() => setRuleAndView(rule)}>{label}</Button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -280,7 +303,7 @@ export const AccessControl = () => {
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Access Control</Title>
|
||||
<Paragraph>Create and control access groups</Paragraph>
|
||||
<Paragraph>Access rules help you manage access permissions in your organisation.</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}>
|
||||
@@ -288,13 +311,13 @@ export const AccessControl = () => {
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
{/*<Radio.Group
|
||||
<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>
|
||||
@@ -314,7 +337,6 @@ export const AccessControl = () => {
|
||||
{failed &&
|
||||
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
|
||||
}
|
||||
{loading && <Loading/>}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{
|
||||
@@ -325,20 +347,34 @@ export const AccessControl = () => {
|
||||
setCurrentPage(page)
|
||||
}
|
||||
}}
|
||||
className={`${showTutorial ? "card-table card-table-no-placeholder" : "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"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).Name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).Name.localeCompare((b as any).Name))} />
|
||||
<Column title="Name" dataIndex="name"
|
||||
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, index) => {
|
||||
const desc = (record as RuleDataTable).description.trim()
|
||||
return <Tooltip title={desc !== "" ? desc : "no description"} arrowPointAtCenter>
|
||||
<span onClick={() => setRuleAndView(record as RuleDataTable)} className="tooltip-label">{text}</span>
|
||||
</Tooltip>
|
||||
}}
|
||||
/>
|
||||
<Column title="Status" dataIndex="disabled"
|
||||
render={(text:Boolean, record:RuleDataTable, index) => {
|
||||
return text ? <Tag color="red">disabled</Tag> : <Tag color="green">enabled</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Sources" dataIndex="sourceLabel"
|
||||
render={(text, record:RuleDataTable, index) => {
|
||||
//return <Button type="link" onClick={() => toggleModalGroups(`${record.Name} - Sources`, record.Source, true)}>{text}</Button>
|
||||
return renderPopoverGroups(text, record.Source)
|
||||
return renderPopoverGroups(text, record.sources,record as RuleDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Direction" dataIndex="Flow"
|
||||
<Column title="Direction" dataIndex="flow"
|
||||
render={(text, record:RuleDataTable, index) => {
|
||||
const s = {minWidth: 50, textAlign: "center"} as React.CSSProperties
|
||||
if (text === "bidirect")
|
||||
@@ -353,13 +389,13 @@ export const AccessControl = () => {
|
||||
/>
|
||||
<Column title="Destinations" dataIndex="destinationLabel"
|
||||
render={(text, record:RuleDataTable, index) => {
|
||||
//return <Button type="link" onClick={() => toggleModalGroups(`${record.Name} - Destinations`, record.Destination, true)}>{text}</Button>
|
||||
return renderPopoverGroups(text, record.Destination)
|
||||
//return <Button type="link" onClick={() => toggleModalGroups(`${record.name} - Destinations`, record.destinations, true)}>{text}</Button>
|
||||
return renderPopoverGroups(text, record.destinations,record as RuleDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="" align="center"
|
||||
render={(text, record, index) => {
|
||||
if (dataTable.length === 1 || deletedRule.loading || savedRule.loading) return <></>
|
||||
if (deletedRule.loading || savedRule.loading) return <></>
|
||||
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
|
||||
onVisibleChange={visible => {
|
||||
if (visible) setRuleToAction(record as RuleDataTable)
|
||||
@@ -370,11 +406,6 @@ export const AccessControl = () => {
|
||||
{showTutorial &&
|
||||
<Space direction="vertical" size="small" align="center"
|
||||
style={{display: 'flex', padding: '45px 15px'}}>
|
||||
<img src={tutorial} style={{width: 362, paddingBottom: 45}}/>
|
||||
<Title level={5}>Create and control access groups</Title>
|
||||
<Paragraph>
|
||||
Access rules help you manage access permissions in your organisation.
|
||||
</Paragraph>
|
||||
<Button type="link" onClick={onClickAddNewRule}>Add new access rule</Button>
|
||||
</Space>
|
||||
}
|
||||
|
||||
@@ -20,21 +20,25 @@ import {
|
||||
RadioChangeEvent,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Alert, Select, Modal, Button, message
|
||||
Alert, Select, Modal, Button, message, Popover, SpinProps, Spin
|
||||
} 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 ButtonCopyMessage from "../components/ButtonCopyMessage";
|
||||
import {Group} from "../store/group/types";
|
||||
import {Group, GroupPeer} from "../store/group/types";
|
||||
import PeerGroupsUpdate from "../components/PeerGroupsUpdate";
|
||||
import tableSpin from "../components/Spin";
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { Column } = Table;
|
||||
const { confirm } = Modal;
|
||||
|
||||
interface PeerDataTable extends Peer {
|
||||
key: string
|
||||
key: string;
|
||||
groups: Group[];
|
||||
groupsCount: number;
|
||||
}
|
||||
|
||||
export const Peers = () => {
|
||||
@@ -47,6 +51,7 @@ export const Peers = () => {
|
||||
const deletedPeer = useSelector((state: RootState) => state.peer.deletedPeer);
|
||||
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 [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionOnOff, setOptionOnOff] = useState('all');
|
||||
@@ -60,7 +65,7 @@ export const Peers = () => {
|
||||
{label: "15", value: "15"}
|
||||
]
|
||||
|
||||
const optionsOnOff = [{label: 'All', value: 'all'}, {label: 'Online', value: 'on'}]
|
||||
const optionsOnOff = [{label: 'Online', value: 'on'},{label: 'All', value: 'all'}]
|
||||
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
@@ -71,22 +76,28 @@ export const Peers = () => {
|
||||
const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>)
|
||||
|
||||
const transformDataTable = (d:Peer[]):PeerDataTable[] => {
|
||||
return d.map(p => ({ key: p.IP, ...p } as PeerDataTable))
|
||||
const peer_ids = d.map(_p => _p.id)
|
||||
return d.map((p) => {
|
||||
const gs = groups
|
||||
.filter(g => g.peers?.find((_p:GroupPeer) => _p.id === p.id))
|
||||
.map(g => ({id: g.id, name: g.name, peers_count: g.peers?.length, peers: g.peers || []}))
|
||||
return {
|
||||
key: p.id,
|
||||
...p,
|
||||
groups: gs,
|
||||
groupsCount: gs.length
|
||||
} as PeerDataTable
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(peerActions.getPeers.request({getAccessTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently, payload: null}));
|
||||
// dispatch(groupActions.saveGroup.request({getAccessTokenSilently, payload: {
|
||||
// ID: "caakdnhvdm4s73ak8cgg",
|
||||
// Name: "ZZZZ",
|
||||
// Peers: ["wksAVnZSc/ewyU8f8UFqQjjb3TKOqgxSVa0FtDz0jHs="]
|
||||
// } as Group}))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(peers))
|
||||
}, [peers])
|
||||
}, [peers, groups])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
@@ -99,18 +110,35 @@ export const Peers = () => {
|
||||
message.loading({ content: 'Deleting...', key: deleteKey, style });
|
||||
} else if (deletedPeer.success) {
|
||||
message.success({ content: 'Peer has been successfully removed.', key: deleteKey, duration: 2, style });
|
||||
dispatch(peerActions.resetDeletedPeer(null))
|
||||
} else if (deletedPeer.error) {
|
||||
message.error({ content: 'Failed to delete peer. You might not have enough permissions.', key: deleteKey, duration: 2, style });
|
||||
dispatch(peerActions.resetDeletedPeer(null))
|
||||
}
|
||||
}, [deletedPeer])
|
||||
|
||||
const saveGroupsKey = 'saving_groups';
|
||||
useEffect(() => {
|
||||
const style = { marginTop: 85 }
|
||||
if (savedGroups.loading) {
|
||||
message.loading({ content: 'Updating peer groups...', key: saveGroupsKey, style });
|
||||
} else if (savedGroups.success) {
|
||||
message.success({ content: 'Peer groups have been successfully updated.', key: saveGroupsKey, duration: 2, style });
|
||||
setUpdateGroupsVisible({} as Peer, false)
|
||||
dispatch(peerActions.resetSavedGroups(null))
|
||||
} else if (savedGroups.error) {
|
||||
message.error({ content: 'Failed to update peer groups. You might not have enough permissions.', key: saveGroupsKey, duration: 2, style });
|
||||
dispatch(peerActions.resetSavedGroups(null))
|
||||
}
|
||||
}, [savedGroups])
|
||||
|
||||
const filterDataTable = ():Peer[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f:Peer[] = filter(peers, (f:Peer) =>
|
||||
(f.Name.toLowerCase().includes(t) || f.IP.includes(t) || f.OS.includes(t) || t === "")
|
||||
(f.name.toLowerCase().includes(t) || f.ip.includes(t) || f.os.includes(t) || t === "")
|
||||
) as Peer[]
|
||||
if (optionOnOff === "on") {
|
||||
f = filter(peers, (f:Peer) => f.Connected)
|
||||
f = filter(peers, (f:Peer) => f.connected)
|
||||
}
|
||||
return f
|
||||
}
|
||||
@@ -139,14 +167,14 @@ export const Peers = () => {
|
||||
content: <Space direction="vertical" size="small">
|
||||
{peerToAction &&
|
||||
<>
|
||||
<Title level={5}>Delete peer "{peerToAction ? peerToAction.Name : ''}"</Title>
|
||||
<Title level={5}>Delete peer "{peerToAction ? peerToAction.name : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to delete peer from your account?</Paragraph>
|
||||
</>
|
||||
}
|
||||
</Space>,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(peerActions.deletedPeer.request({getAccessTokenSilently, payload: peerToAction ? peerToAction.IP : ''}));
|
||||
dispatch(peerActions.deletedPeer.request({getAccessTokenSilently, payload: peerToAction ? peerToAction.ip : ''}));
|
||||
},
|
||||
onCancel() {
|
||||
setPeerToAction(null);
|
||||
@@ -154,99 +182,143 @@ export const Peers = () => {
|
||||
});
|
||||
}
|
||||
|
||||
const setUpdateGroupsVisible = (peerToAction:Peer, status:boolean) => {
|
||||
if (status) {
|
||||
dispatch(peerActions.setPeer({...peerToAction}))
|
||||
dispatch(peerActions.setUpdateGroupsVisible(true))
|
||||
return
|
||||
}
|
||||
dispatch(peerActions.setPeer(null))
|
||||
dispatch(peerActions.setUpdateGroupsVisible(false))
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, groups:Group[] | string[] | null, peerToAction:PeerDataTable) => {
|
||||
const content = groups?.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>)
|
||||
return (
|
||||
<Popover key={peerToAction.key} content={mainContent} title={null}>
|
||||
<Button type="link" onClick={() => setUpdateGroupsVisible(peerToAction, true)}>{label}</Button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Peers</Title>
|
||||
<Paragraph>A list of all the machines in your account including their name, IP and status.</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.Search allowClear value={textToSearch} onPressEnter={searchDataTable} onSearch={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />*/}
|
||||
<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={optionsOnOff}
|
||||
onChange={onChangeOnOff}
|
||||
value={optionOnOff}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Peers</Title>
|
||||
<Paragraph>A list of all the machines in your account including their name, IP and status.</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.Search allowClear value={textToSearch} onPressEnter={searchDataTable} onSearch={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />*/}
|
||||
<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={optionsOnOff}
|
||||
onChange={onChangeOnOff}
|
||||
value={optionOnOff}
|
||||
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>
|
||||
<Link to="/add-peer" className="ant-btn ant-btn-primary ant-btn-block">Add Peer</Link>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
|
||||
}
|
||||
{/*{loading && <Loading/>}*/}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{pageSize, showSizeChanger: false, showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} peers`)}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Name" dataIndex="name"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
defaultSortOrder='ascend'
|
||||
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))} />
|
||||
<Column title="IP" dataIndex="ip"
|
||||
sorter={(a, b) => {
|
||||
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 <ButtonCopyMessage keyMessage={(record as PeerDataTable).key} text={text} messageText={'IP copied!'} styleNotification={{}}/>
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
<Link to="/add-peer" className="ant-btn ant-btn-primary ant-btn-block">Add Peer</Link>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
|
||||
}
|
||||
{loading && <Loading/>}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{pageSize, showSizeChanger: false, showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} peers`)}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Name" dataIndex="Name" key="Name"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).Name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).Name.localeCompare((b as any).Name))} />
|
||||
<Column title="IP" dataIndex="IP"
|
||||
sorter={(a, b) => {
|
||||
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 <ButtonCopyMessage keyMessage={(record as PeerDataTable).key} text={text} messageText={'IP copied!'} styleNotification={{}}/>
|
||||
}}
|
||||
/>
|
||||
<Column title="Status" dataIndex="Connected"
|
||||
render={(text, record, index) => {
|
||||
return text ? <Tag color="green">online</Tag> : <Tag color="red">offline</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="LastSeen" dataIndex="LastSeen"
|
||||
render={(text, record, index) => {
|
||||
return (record as PeerDataTable).Connected ? 'just now' : timeAgo(text)
|
||||
}}
|
||||
/>
|
||||
<Column title="OS" dataIndex="OS"
|
||||
render={(text, record, index) => {
|
||||
return formatOS(text)
|
||||
}}
|
||||
/>
|
||||
<Column title="Version" dataIndex="Version" />
|
||||
<Column title="" align="center"
|
||||
render={(text, record, index) => {
|
||||
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
|
||||
onVisibleChange={visible => {
|
||||
if (visible) setPeerToAction(record as PeerDataTable)
|
||||
}}></Dropdown.Button>
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<Column title="Status" dataIndex="connected"
|
||||
render={(text, record, index) => {
|
||||
return text ? <Tag color="green">online</Tag> : <Tag color="red">offline</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Groups" dataIndex="groupsCount"
|
||||
render={(text, record:PeerDataTable, index) => {
|
||||
return renderPopoverGroups(text, record.groups, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="LastSeen" dataIndex="last_seen"
|
||||
render={(text, record, index) => {
|
||||
return (record as PeerDataTable).connected ? 'just now' : timeAgo(text)
|
||||
}}
|
||||
/>
|
||||
<Column title="OS" dataIndex="os"
|
||||
render={(text, record, index) => {
|
||||
return formatOS(text)
|
||||
}}
|
||||
/>
|
||||
<Column title="Version" dataIndex="version" />
|
||||
<Column title="" align="center"
|
||||
render={(text, record, index) => {
|
||||
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
|
||||
onVisibleChange={visible => {
|
||||
if (visible) setPeerToAction(record as PeerDataTable)
|
||||
}}></Dropdown.Button>
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<PeerGroupsUpdate/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = (<Menu items={itemsMenuAction} ></Menu>)
|
||||
|
||||
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: <Space direction="vertical" size="small">
|
||||
{setupKeyToAction &&
|
||||
<>
|
||||
<Title level={5}>Delete setupKey "{setupKeyToAction ? setupKeyToAction.Name : ''}"</Title>
|
||||
<Title level={5}>Delete setupKey "{setupKeyToAction ? setupKeyToAction.name : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to delete key?</Paragraph>
|
||||
</>
|
||||
}
|
||||
</Space>,
|
||||
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: <Space direction="vertical" size="small">
|
||||
{setupKeyToAction &&
|
||||
<>
|
||||
<Title level={5}>Revoke setupKey "{setupKeyToAction ? setupKeyToAction.Name : ''}"</Title>
|
||||
<Title level={5}>Revoke setupKey "{setupKeyToAction ? setupKeyToAction.name : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to revoke key?</Paragraph>
|
||||
</>
|
||||
}
|
||||
</Space>,
|
||||
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 &&
|
||||
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
|
||||
}
|
||||
{loading && <Loading/>}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{pageSize, showSizeChanger: false, showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} setup keys`)}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Name" dataIndex="Name"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).Name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).Name.localeCompare((b as any).Name))}
|
||||
<Column title="Name" dataIndex="name"
|
||||
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'
|
||||
/>
|
||||
|
||||
<Column title="State" dataIndex="State"
|
||||
<Column title="State" dataIndex="state"
|
||||
render={(text, record, index) => {
|
||||
return (text === 'valid') ? <Tag color="green">{text}</Tag> : <Tag color="red">{text}</Tag>
|
||||
}}
|
||||
sorter={(a, b) => ((a as any).State.localeCompare((b as any).State))}
|
||||
sorter={(a, b) => ((a as any).state.localeCompare((b as any).state))}
|
||||
/>
|
||||
|
||||
<Column title="Type" dataIndex="Type"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).Type.includes(value)}
|
||||
sorter={(a, b) => ((a as any).Type.localeCompare((b as any).Type))}
|
||||
<Column title="Type" dataIndex="type"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).type.includes(value)}
|
||||
sorter={(a, b) => ((a as any).type.localeCompare((b as any).type))}
|
||||
/>
|
||||
|
||||
<Column title="Key" dataIndex="Key"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).Key.includes(value)}
|
||||
sorter={(a, b) => ((a as any).Key.localeCompare((b as any).Key))}
|
||||
<Column title="Key" dataIndex="key"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).key.includes(value)}
|
||||
sorter={(a, b) => ((a as any).key.localeCompare((b as any).key))}
|
||||
render={(text, record, index) => {
|
||||
return <ButtonCopyMessage keyMessage={(record as SetupKeyDataTable).key} text={text} messageText={`Key copied!`} styleNotification={{}}/>
|
||||
}}
|
||||
/>
|
||||
|
||||
<Column title="Last Used" dataIndex="LastUsed"
|
||||
<Column title="Last Used" dataIndex="last_used"
|
||||
sorter={(a, b) => ((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)
|
||||
}}
|
||||
/>
|
||||
<Column title="Used Times" dataIndex="UsedTimes"
|
||||
sorter={(a, b) => ((a as any).Type.localeCompare((b as any).Type))}
|
||||
<Column title="Used Times" dataIndex="used_times"
|
||||
sorter={(a, b) => ((a as any).used_times - ((b as any).used_times))}
|
||||
/>
|
||||
|
||||
<Column title="Expires" dataIndex="Expires"
|
||||
<Column title="Expires" dataIndex="expires"
|
||||
render={(text, record, index) => {
|
||||
return formatDate(text)
|
||||
}}
|
||||
@@ -302,7 +308,7 @@ export const SetupKeys = () => {
|
||||
|
||||
<Column title="" align="center"
|
||||
render={(text, record, index) => {
|
||||
return !(record as SetupKeyDataTable).Revoked ? (
|
||||
return !(record as SetupKeyDataTable).revoked ? (
|
||||
<Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
|
||||
onVisibleChange={visible => {
|
||||
if (visible) setSetupKeyToAction(record as SetupKeyDataTable)
|
||||
|
||||
@@ -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 &&
|
||||
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
|
||||
}
|
||||
{loading && <Loading/>}
|
||||
<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}>
|
||||
<Column title="Email" dataIndex="email"
|
||||
onFilter={(value: string | number | boolean, record) => (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
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user