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:
Misha Bragin
2022-06-15 11:42:15 +02:00
committed by GitHub
parent 4e78fabb35
commit 6724e5bfaa
30 changed files with 894 additions and 337 deletions

View File

@@ -9,6 +9,8 @@ import Loading from "./components/Loading";
import SetupKeys from "./views/SetupKeys"; import SetupKeys from "./views/SetupKeys";
import AddPeer from "./views/AddPeer"; import AddPeer from "./views/AddPeer";
import Users from './views/Users'; import Users from './views/Users';
import AccessControl from './views/AccessControl';
// import Activity from './views/Activity';
import Banner from "./components/Banner"; import Banner from "./components/Banner";
import {store} from "./store"; import {store} from "./store";
@@ -111,8 +113,8 @@ function App() {
<Route path='/peers' exact component={Peers}/> <Route path='/peers' exact component={Peers}/>
<Route path="/add-peer" component={AddPeer}/> <Route path="/add-peer" component={AddPeer}/>
<Route path="/setup-keys" component={SetupKeys}/> <Route path="/setup-keys" component={SetupKeys}/>
{/*<Route path="/acls" component={AccessControl}/> <Route path="/acls" component={AccessControl}/>
<Route path="/activity" component={Activity}/>*/} {/*<Route path="/activity" component={Activity}/>*/}
<Route path="/users" component={Users}/> <Route path="/users" component={Users}/>
</Switch> </Switch>
</Content> </Content>

View File

@@ -27,9 +27,9 @@ const AccessControlModalGroups:React.FC<Props> = ({data, title, visible, onCance
renderItem={(item:Group) => ( renderItem={(item:Group) => (
<List.Item> <List.Item>
<List.Item.Meta <List.Item.Meta
avatar={<Avatar>{item.Name.slice(0,1).toUpperCase()}</Avatar>} avatar={<Avatar>{item.name.slice(0,1).toUpperCase()}</Avatar>}
title={item.Name} title={item.name}
description={`${item.PeersCount} peers`} description={`${item.peers_count} peers`}
/> />
</List.Item> </List.Item>
)} )}

View File

@@ -2,19 +2,16 @@ import React, {useEffect, useRef, useState} from 'react';
import {useDispatch, useSelector} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions"; import {RootState} from "typesafe-actions";
import { actions as ruleActions } from '../store/rule'; 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 { import {
Col, Col,
Row, Row,
Typography, Typography,
Input, Input,
Space, Space,
Radio, Switch,
Button, Drawer, Form, List, Divider, Select, Tag Button, Drawer, Form, Divider, Select, Tag, Radio, RadioChangeEvent
} from "antd"; } 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 type { CustomTagProps } from 'rc-select/lib/BaseSelect'
import {Rule, RuleToSave} from "../store/rule/types"; import {Rule, RuleToSave} from "../store/rule/types";
import {useAuth0} from "@auth0/auth0-react"; import {useAuth0} from "@auth0/auth0-react";
@@ -38,10 +35,14 @@ const AccessControlNew = () => {
const savedRule = useSelector((state: RootState) => state.rule.savedRule) const savedRule = useSelector((state: RootState) => state.rule.savedRule)
const [editName, setEditName] = useState(false) const [editName, setEditName] = useState(false)
const [editDescription, setEditDescription] = useState(false)
const [tagGroups, setTagGroups] = useState([] as string[]) const [tagGroups, setTagGroups] = useState([] as string[])
const [formRule, setFormRule] = useState({} as FormRule) const [formRule, setFormRule] = useState({} as FormRule)
const [form] = Form.useForm() const [form] = Form.useForm()
const inputNameRef = useRef<any>(null) const inputNameRef = useRef<any>(null)
const inputDescriptionRef = useRef<any>(null)
const optionsDisabledEnabled = [{label: 'Enabled', value: false}, {label: 'Disabled', value: true}]
useEffect(() => { useEffect(() => {
if (editName) inputNameRef.current!.focus({ if (editName) inputNameRef.current!.focus({
@@ -49,36 +50,44 @@ const AccessControlNew = () => {
}); });
}, [editName]); }, [editName]);
useEffect(() => {
if (editDescription) inputDescriptionRef.current!.focus({
cursor: 'end',
});
}, [editDescription]);
useEffect(() => { useEffect(() => {
if (!rule) return if (!rule) return
const fRule = { const fRule = {
...rule, ...rule,
tagSourceGroups: rule.Source ? rule.Source?.map(t => t.Name) : [], tagSourceGroups: rule.sources ? rule.sources?.map(t => t.name) : [],
tagDestinationGroups: rule.Destination ? rule.Destination?.map(t => t.Name) : [] tagDestinationGroups: rule.destinations ? rule.destinations?.map(t => t.name) : []
} as FormRule } as FormRule
setFormRule(fRule) setFormRule(fRule)
form.setFieldsValue(fRule) form.setFieldsValue(fRule)
}, [rule]) }, [rule])
useEffect(() => { useEffect(() => {
setTagGroups(groups?.map(g => g.Name) || []) setTagGroups(groups?.map(g => g.name) || [])
}, [groups]) }, [groups])
const createRuleToSave = ():RuleToSave => { const createRuleToSave = ():RuleToSave => {
const Source = groups?.filter(g => formRule.tagSourceGroups.includes(g.Name)).map(g => g.ID || '') || [] const sources = 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 destinations = groups?.filter(g => formRule.tagDestinationGroups.includes(g.name)).map(g => g.id || '') || []
const sourcesNoId = formRule.tagSourceGroups.filter(s => !tagGroups.includes(s)) const sourcesNoId = formRule.tagSourceGroups.filter(s => !tagGroups.includes(s))
const destinationsNoId = formRule.tagDestinationGroups.filter(s => !tagGroups.includes(s)) const destinationsNoId = formRule.tagDestinationGroups.filter(s => !tagGroups.includes(s))
const groupsToSave = uniq([...sourcesNoId, ...destinationsNoId]) const groupsToSave = uniq([...sourcesNoId, ...destinationsNoId])
return { return {
ID: formRule.ID, id: formRule.id,
Name: formRule.Name, name: formRule.name,
Source, description: formRule.description,
Destination, sources,
destinations,
sourcesNoId, sourcesNoId,
destinationsNoId, destinationsNoId,
groupsToSave, groupsToSave,
Flow: formRule.Flow flow: formRule.flow,
disabled: formRule.disabled
} as RuleToSave } as RuleToSave
} }
@@ -101,10 +110,12 @@ const AccessControlNew = () => {
if (savedRule.loading) return if (savedRule.loading) return
setEditName(false) setEditName(false)
dispatch(ruleActions.setRule({ dispatch(ruleActions.setRule({
Name: '', name: '',
Source: [], description: '',
Destination: [], sources: [],
Flow: 'bidirect' destinations: [],
flow: 'bidirect',
disabled: false
} as Rule)) } as Rule))
setVisibleNewRule(false) setVisibleNewRule(false)
} }
@@ -127,6 +138,13 @@ const AccessControlNew = () => {
}) })
}; };
const handleChangeDisabled = ({ target: { value } }: RadioChangeEvent) => {
setFormRule({
...formRule,
disabled: value
})
};
const tagRender = (props: CustomTagProps) => { const tagRender = (props: CustomTagProps) => {
const { label, value, closable, onClose } = props; const { label, value, closable, onClose } = props;
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => { const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
@@ -149,8 +167,8 @@ const AccessControlNew = () => {
const optionRender = (label: string) => { const optionRender = (label: string) => {
let peersCount = '' let peersCount = ''
const g = groups.find(_g => _g.Name === label) const g = groups.find(_g => _g.name === label)
if (g) peersCount = ` - ${g.PeersCount || 0} ${(g.PeersCount && parseInt(g.PeersCount) > 1) ? 'peers' : 'peer'} ` if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
return ( return (
<> <>
<Tag <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) => { const toggleEditName = (status:boolean) => {
setEditName(status); setEditName(status);
} }
const toggleEditDescription = (status:boolean) => {
setEditDescription(status);
}
// const testDeleteGroup = () => { // const testDeleteGroup = () => {
// groups.forEach(g => { // groups.forEach(g => {
// dispatch(groupsActions.deleteGroup.request({getAccessTokenSilently, payload: g.ID || ''})) // dispatch(groupsActions.deleteGroup.request({getAccessTokenSilently, payload: g.ID || ''}))
@@ -185,10 +224,11 @@ const AccessControlNew = () => {
visible={setupNewRuleVisible} visible={setupNewRuleVisible}
bodyStyle={{paddingBottom: 80}} bodyStyle={{paddingBottom: 80}}
onClose={onCancel} onClose={onCancel}
autoFocus={true}
footer={ footer={
<Space style={{display: 'flex', justifyContent: 'end'}}> <Space style={{display: 'flex', justifyContent: 'end'}}>
<Button onClick={onCancel} disabled={savedRule.loading}>Cancel</Button> <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> </Space>
} }
> >
@@ -198,7 +238,7 @@ const AccessControlNew = () => {
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}> <Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
<Row align="top"> <Row align="top">
<Col flex="none" style={{display: "flex"}}> <Col flex="none" style={{display: "flex"}}>
{!editName && formRule.ID && {!editName && !editDescription && formRule.id &&
<button type="button" aria-label="Close" className="ant-drawer-close" <button type="button" aria-label="Close" className="ant-drawer-close"
style={{paddingTop: 3}} style={{paddingTop: 3}}
onClick={onCancel}> onClick={onCancel}>
@@ -209,23 +249,61 @@ const AccessControlNew = () => {
} }
</Col> </Col>
<Col flex="auto"> <Col flex="auto">
{ !editName && formRule.ID ? ( { !editName && formRule.id ? (
<div className={"access-control ant-drawer-title"} onClick={() => toggleEditName(true)}>{formRule.ID ? formRule.Name : 'New Rule'}</div> <div className={"access-control input-text ant-drawer-title"} onClick={() => toggleEditName(true)}>{formRule.id ? formRule.name : 'New Rule'}</div>
) : ( ) : (
<Form.Item <Form.Item
name="Name" name="name"
label={null} label="Name"
rules={[{required: true, message: 'Please add a name for this access rule'}]} 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"/> <Input placeholder="Add rule name..." ref={inputNameRef} onPressEnter={() => toggleEditName(false)} onBlur={() => toggleEditName(false)} autoComplete="off"/>
</Form.Item> </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> </Col>
</Row> </Row>
</Header> </Header>
</Col> </Col>
<Col span={24}> <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>
<Col span={24}> <Col span={24}>
<Form.Item <Form.Item
@@ -234,7 +312,13 @@ const AccessControlNew = () => {
rules={[{required: true, message: 'Please enter ate least one group'}]} rules={[{required: true, message: 'Please enter ate least one group'}]}
style={{display: 'flex'}} 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 => tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option> <Option key={m}>{optionRender(m)}</Option>
@@ -250,7 +334,13 @@ const AccessControlNew = () => {
rules={[{required: true, message: 'Please enter ate least one group'}]} rules={[{required: true, message: 'Please enter ate least one group'}]}
style={{display: 'flex'}} 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 => tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option> <Option key={m}>{optionRender(m)}</Option>
@@ -271,7 +361,6 @@ const AccessControlNew = () => {
<Paragraph> <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. 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> </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> </Col>
</Row> </Row>
</Col> </Col>
@@ -279,7 +368,7 @@ const AccessControlNew = () => {
<Divider></Divider> <Divider></Divider>
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank" <Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
href="https://docs.netbird.io/docs/overview/acls" style={{color: 'rgb(07, 114, 128)'}}>Learn 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> </Col>
</Row> </Row>
</Form> </Form>

View File

@@ -5,8 +5,8 @@ import {useAuth0} from "@auth0/auth0-react";
import {useLocation} from 'react-router-dom'; import {useLocation} from 'react-router-dom';
import {Menu, Row, Col, Grid, Dropdown, Avatar, Button, Typography, Space} from 'antd' import {Menu, Row, Col, Grid, Dropdown, Avatar, Button, Typography, Space} from 'antd'
import {ItemType} from "antd/lib/menu/hooks/useItems"; import {ItemType} from "antd/lib/menu/hooks/useItems";
import {UserOutlined} from "@ant-design/icons";
import {AvatarSize} from "antd/es/avatar/SizeContext"; import {AvatarSize} from "antd/es/avatar/SizeContext";
import { UserOutlined } from '@ant-design/icons';
const { Text } = Typography const { Text } = Typography
const { useBreakpoint } = Grid; const { useBreakpoint } = Grid;
@@ -30,8 +30,8 @@ const Navbar = () => {
{ label: (<Link to="/peers">Peers</Link>), key: '/peers' }, { label: (<Link to="/peers">Peers</Link>), key: '/peers' },
{ label: (<Link to="/add-peer">Add Peer</Link>), key: '/add-peer' }, { 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="/setup-keys">Setup Keys</Link>), key: '/setup-keys' },
/*{ label: (<Link to="/acls">Access Control</Link>), key: '/acls' }, { label: (<Link to="/acls">Access Control</Link>), key: '/acls' },
{ label: (<Link to="/activity">Activity</Link>), key: '/activity' },*/ // { label: (<Link to="/activity">Activity</Link>), key: '/activity' },
{ label: (<Link to="/users">Users</Link>), key: '/users' } { label: (<Link to="/users">Users</Link>), key: '/users' }
] as ItemType[]) ] as ItemType[])
@@ -80,7 +80,7 @@ const Navbar = () => {
const createAvatar = (size:AvatarSize) => { const createAvatar = (size:AvatarSize) => {
return user?.picture ? ( 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> <Avatar size={size}>{(user?.name || '').slice(0, 1).toUpperCase()}</Avatar>
) )

View 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

View File

@@ -48,8 +48,8 @@ const SetupKeyNew = () => {
const onCancel = () => { const onCancel = () => {
if (createdSetupKey.loading) return if (createdSetupKey.loading) return
dispatch(setupKeyActions.setSetupKey({ dispatch(setupKeyActions.setSetupKey({
Name: '', name: '',
Type: 'reusable' type: 'reusable'
} as SetupKey)) } as SetupKey))
setVisibleNewSetupKey(false) setVisibleNewSetupKey(false)
} }

12
src/components/Spin.tsx Normal file
View 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;

View File

@@ -122,7 +122,17 @@ body {
color: rgba(0, 0, 0, .85) !important; color: rgba(0, 0, 0, .85) !important;
} }
.access-control.ant-drawer-title:hover { .access-control-table .tooltip-label:hover {
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;
}
.access-control.input-text:hover {
text-decoration: underline;
cursor: pointer;
}
.access-control.ant-drawer-subtitle {
line-height: 22px;
margin: 24px 0;
} }

View File

@@ -25,19 +25,6 @@ const providerConfig = {
onRedirectCallback, onRedirectCallback,
}; };
/*
ReactDOM.render(
<Auth0Provider {...providerConfig}>
<React.StrictMode>
<BrowserRouter>
<App/>
</BrowserRouter>
</React.StrictMode>
</Auth0Provider>,
document.getElementById('root')
);
*/
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
); );

View File

@@ -39,7 +39,7 @@ export function* saveGroup(action: ReturnType<typeof actions.saveGroup.request>)
} as CreateResponse<Group | null>)) } as CreateResponse<Group | null>))
let effect let effect
if (action.payload.payload.ID) { if (action.payload.payload.id) {
effect = yield call(service.editGroup, action.payload); effect = yield call(service.editGroup, action.payload);
} else { } else {
effect = yield call(service.createGroup, action.payload); effect = yield call(service.createGroup, action.payload);
@@ -94,7 +94,7 @@ export function* deleteGroup(action: ReturnType<typeof actions.deleteGroup.reque
} as DeleteResponse<string | null>)); } as DeleteResponse<string | null>));
const rules = (yield select(state => state.rule.data)) as Group[] 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) { } catch (err) {
yield put(actions.deleteGroup.failure({ yield put(actions.deleteGroup.failure({
loading: false, loading: false,

View File

@@ -29,8 +29,10 @@ export default {
); );
}, },
async editGroup(payload:RequestPayload<Group>): Promise<ApiResponse<Group>> { async editGroup(payload:RequestPayload<Group>): Promise<ApiResponse<Group>> {
const id = payload.payload.id
delete payload.payload.id
return apiClient.put<Group>( return apiClient.put<Group>(
`${baseUrl}`, `${baseUrl}/${id}`,
payload payload
); );
}, },

View File

@@ -1,6 +1,11 @@
export interface Group { export interface Group {
ID?: string; id?: string;
Name: string; name: string;
Peers?: any[]; peers?: GroupPeer[] | string[];
PeersCount?: string; peers_count?: string;
}
export interface GroupPeer {
id: string,
name: string
} }

View File

@@ -1,6 +1,7 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions'; import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import { Peer } from './types'; import {Peer, PeerGroupsToSave} from './types';
import {ApiError, DeleteResponse, RequestPayload} from '../../services/api-client/types'; import {ApiError, ChangeResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
import {Group} from "../group/types";
const actions = { const actions = {
getPeers: createAsyncAction( getPeers: createAsyncAction(
@@ -8,14 +9,26 @@ const actions = {
'GET_PEERS_SUCCESS', 'GET_PEERS_SUCCESS',
'GET_PEERS_FAILURE', 'GET_PEERS_FAILURE',
)<RequestPayload<null>, Peer[], ApiError>(), )<RequestPayload<null>, Peer[], ApiError>(),
deletedPeer: createAsyncAction( deletedPeer: createAsyncAction(
'DELETE_PEER_REQUEST', 'DELETE_PEER_REQUEST',
'DELETE_PEER_SUCCESS', 'DELETE_PEER_SUCCESS',
'DELETE_PEER_FAILURE' 'DELETE_PEER_FAILURE'
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(), )<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
resetDeletedPeer: createAction('RESET_DELETED_PEER')<null>(),
setDeletePeer: createAction('SET_DELETE_PEER')<DeleteResponse<string | 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>(), 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>; export type ActionTypes = ActionType<typeof actions>;

View File

@@ -2,15 +2,18 @@ import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { Peer } from './types'; import { Peer } from './types';
import actions, { ActionTypes } from './actions'; 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<{ type StateType = Readonly<{
data: Peer[] | null; data: Peer[] | null;
peer: Peer | null; peer?: Peer | null;
loading: boolean; loading: boolean;
failed: ApiError | null; failed: ApiError | null;
saving: boolean; saving: boolean;
deletedPeer: DeleteResponse<string | null>; deletedPeer: DeleteResponse<string | null>;
setUpdateGroupsVisible: boolean;
savedGroups: ChangeResponse<Group[] | null>;
}>; }>;
const initialState: StateType = { const initialState: StateType = {
@@ -25,6 +28,14 @@ const initialState: StateType = {
failure: false, failure: false,
error: null, error: null,
data : 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.success,(_, action) => action.payload)
.handleAction(actions.getPeers.failure, () => []); .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); .handleAction(actions.setPeer, (store, action) => action.payload);
const loading = createReducer<boolean, ActionTypes>(initialState.loading) 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.success, (store, action) => action.payload)
.handleAction(actions.deletedPeer.failure, (store, action) => action.payload) .handleAction(actions.deletedPeer.failure, (store, action) => action.payload)
.handleAction(actions.setDeletePeer, (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({ export default combineReducers({
data, data,
@@ -62,5 +83,7 @@ export default combineReducers({
loading, loading,
failed, failed,
saving, saving,
deletedPeer deletedPeer,
updateGroupsVisible,
savedGroups
}); });

View File

@@ -1,8 +1,19 @@
import {all, call, put, select, takeLatest} from 'redux-saga/effects'; import {all, call, spawn, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types'; import {
ApiError,
ApiResponse,
ChangeResponse,
CreateResponse,
DeleteResponse,
RequestPayload
} from '../../services/api-client/types';
import { Peer } from './types' import { Peer } from './types'
import service from './service'; import service from './service';
import actions from './actions'; 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 { export function* getPeers(action: ReturnType<typeof actions.getPeers.request>): Generator {
try { try {
@@ -17,7 +28,6 @@ export function* getPeers(action: ReturnType<typeof actions.getPeers.request>):
const effect = yield call(service.getPeers, action.payload); const effect = yield call(service.getPeers, action.payload);
const response = effect as ApiResponse<Peer[]>; const response = effect as ApiResponse<Peer[]>;
yield put(actions.getPeers.success(response.body)); yield put(actions.getPeers.success(response.body));
} catch (err) { } catch (err) {
yield put(actions.getPeers.failure(err as ApiError)); 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>)); } as DeleteResponse<string | null>));
const peers = (yield select(state => state.peer.data)) as Peer[] 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) { } catch (err) {
yield put(actions.deletedPeer.failure({ yield put(actions.deletedPeer.failure({
loading: false, 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 { export default function* sagas(): Generator {
yield all([ yield all([
takeLatest(actions.getPeers.request, getPeers), takeLatest(actions.getPeers.request, getPeers),
takeLatest(actions.deletedPeer.request, deletePeer) takeLatest(actions.deletedPeer.request, deletePeer),
takeLatest(actions.saveGroups.request, saveGroups)
]); ]);
} }

View File

@@ -1,9 +1,23 @@
import {Group} from "../group/types";
export interface Peer { export interface Peer {
Name: string, id?: string,
IP: string, name: string,
Connected: boolean, ip: string,
LastSeen: string, connected: boolean,
OS: string, last_seen: string,
Version: string, os: string,
Groups?: any[] version: string,
groups?: Group[]
}
export interface PeerToSave extends Peer {
groupsToSave: string[]
}
export interface PeerGroupsToSave {
ID: string;
groupsToRemove: string[];
groupsToAdd: string[];
groupsNoId: string[];
} }

View File

@@ -15,6 +15,7 @@ const actions = {
'SAVE_RULE_FAILURE', 'SAVE_RULE_FAILURE',
)<RequestPayload<RuleToSave>, CreateResponse<Rule | null>, CreateResponse<Rule | null>>(), )<RequestPayload<RuleToSave>, CreateResponse<Rule | null>, CreateResponse<Rule | null>>(),
setSavedRule: createAction('SET_CREATE_RULE')<CreateResponse<Rule | null>>(), setSavedRule: createAction('SET_CREATE_RULE')<CreateResponse<Rule | null>>(),
resetSavedRule: createAction('RESET_CREATE_RULE')<null>(),
deleteRule: createAsyncAction( deleteRule: createAsyncAction(
'DELETE_RULE_REQUEST', 'DELETE_RULE_REQUEST',
@@ -22,7 +23,9 @@ const actions = {
'DELETE_RULE_FAILURE' 'DELETE_RULE_FAILURE'
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(), )<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeletedRule: createAction('SET_DELETED_RULE')<DeleteResponse<string | null>>(), setDeletedRule: createAction('SET_DELETED_RULE')<DeleteResponse<string | null>>(),
resetDeletedRule: createAction('RESET_DELETED_RULE')<null>(),
removeRule: createAction('REMOVE_RULE')<string>(), removeRule: createAction('REMOVE_RULE')<string>(),
setRule: createAction('SET_RULE')<Rule>(), setRule: createAction('SET_RULE')<Rule>(),
setSetupNewRuleVisible: createAction('SET_SETUP_NEW_RULE_VISIBLE')<boolean>() setSetupNewRuleVisible: createAction('SET_SETUP_NEW_RULE_VISIBLE')<boolean>()
}; };

View File

@@ -64,13 +64,15 @@ const deletedRule = createReducer<DeleteResponse<string | null>, ActionTypes>(in
.handleAction(actions.deleteRule.request, () => initialState.deleteRule) .handleAction(actions.deleteRule.request, () => initialState.deleteRule)
.handleAction(actions.deleteRule.success, (store, action) => action.payload) .handleAction(actions.deleteRule.success, (store, action) => action.payload)
.handleAction(actions.deleteRule.failure, (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) const savedRule = createReducer<CreateResponse<Rule | null>, ActionTypes>(initialState.savedRule)
.handleAction(actions.saveRule.request, () => initialState.savedRule) .handleAction(actions.saveRule.request, () => initialState.savedRule)
.handleAction(actions.saveRule.success, (store, action) => action.payload) .handleAction(actions.saveRule.success, (store, action) => action.payload)
.handleAction(actions.saveRule.failure, (store, action) => action.payload) .handleAction(actions.saveRule.failure, (store, action) => action.payload)
.handleAction(actions.setSavedRule, (store, action) => action.payload) .handleAction(actions.setSavedRule, (store, action) => action.payload)
.handleAction(actions.resetSavedRule, () => initialState.savedRule)
const setupNewRuleVisible = createReducer<boolean, ActionTypes>(initialState.setupNewRuleVisible) const setupNewRuleVisible = createReducer<boolean, ActionTypes>(initialState.setupNewRuleVisible)
.handleAction(actions.setSetupNewRuleVisible, (store, action) => action.payload) .handleAction(actions.setSetupNewRuleVisible, (store, action) => action.payload)

View File

@@ -32,7 +32,7 @@ export function* setCreatedRule(action: ReturnType<typeof actions.setSavedRule>
} }
function getNewGroupIds(dataString:string[], responses:Group[]):string[] { 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 { 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, { const responsesGroup = yield all(ruleToSave.groupsToSave.map(g => call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently, 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] const newGroups = [...currentGroups, ...resGroups]
yield put(groupActions.getGroups.success(newGroups)); yield put(groupActions.getGroups.success(newGroups));
console.log(resGroups)
console.log(ruleToSave.groupsToSave)
const newSources = getNewGroupIds(ruleToSave.sourcesNoId, resGroups) const newSources = getNewGroupIds(ruleToSave.sourcesNoId, resGroups)
const newDestinations = getNewGroupIds(ruleToSave.destinationsNoId, resGroups) const newDestinations = getNewGroupIds(ruleToSave.destinationsNoId, resGroups)
console.log(newDestinations)
const payloadToSave = { const payloadToSave = {
getAccessTokenSilently: action.payload.getAccessTokenSilently, getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: { payload: {
Name: ruleToSave.Name, name: ruleToSave.name,
Source: [...ruleToSave.Source as string[], ...newSources], description: ruleToSave.description,
Destination: [...ruleToSave.Destination as string[], ...newDestinations], sources: [...ruleToSave.sources as string[], ...newSources],
Flow: ruleToSave.Flow destinations: [...ruleToSave.destinations as string[], ...newDestinations],
flow: ruleToSave.flow,
disabled: ruleToSave.disabled
} as Rule } as Rule
} }
let effect let effect
if (!ruleToSave.ID) { if (!ruleToSave.id) {
effect = yield call(service.createRule, payloadToSave); effect = yield call(service.createRule, payloadToSave);
} else { } else {
payloadToSave.payload.ID = ruleToSave.ID payloadToSave.payload.id = ruleToSave.id
effect = yield call(service.editRule, payloadToSave); effect = yield call(service.editRule, payloadToSave);
} }
@@ -133,7 +132,7 @@ export function* deleteRule(action: ReturnType<typeof actions.deleteRule.request
} as DeleteResponse<string | null>)); } as DeleteResponse<string | null>));
const rules = (yield select(state => state.rule.data)) as Rule[] 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) { } catch (err) {
yield put(actions.deleteRule.failure({ yield put(actions.deleteRule.failure({
loading: false, loading: false,

View File

@@ -23,8 +23,10 @@ export default {
); );
}, },
async editRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> { async editRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
const id = payload.payload.id
delete payload.payload.id
return apiClient.put<Rule>( return apiClient.put<Rule>(
`/api/rules`, `/api/rules/${id}`,
payload payload
); );
}, },

View File

@@ -1,11 +1,13 @@
import {Group} from "../group/types"; import {Group} from "../group/types";
export interface Rule { export interface Rule {
ID?: string id?: string
Name: string name: string
Source: Group[] | string[] | null description: string
Destination: Group[] | string[] | null sources: Group[] | string[] | null
Flow: string destinations: Group[] | string[] | null
flow: string
disabled: boolean
} }
export interface RuleToSave extends Rule { export interface RuleToSave extends Rule {

View File

@@ -28,13 +28,16 @@ const actions = {
'DELETE_SETUP_KEY_FAILURE' 'DELETE_SETUP_KEY_FAILURE'
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(), )<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeleteSetupKey: createAction('SET_DELETE_SETUP_KEY')<DeleteResponse<string | null>>(), setDeleteSetupKey: createAction('SET_DELETE_SETUP_KEY')<DeleteResponse<string | null>>(),
resetDeletedSetupKey: createAction('RESET_DELETE_SETUP_KEY')<null>(),
revokeSetupKey: createAsyncAction( revokeSetupKey: createAsyncAction(
'REVOKE_SETUP_KEY_REQUEST', 'REVOKE_SETUP_KEY_REQUEST',
'REVOKE_SETUP_KEY_SUCCESS', 'REVOKE_SETUP_KEY_SUCCESS',
'REVOKE_SETUP_KEY_FAILURE' 'REVOKE_SETUP_KEY_FAILURE'
)<RequestPayload<SetupKeyRevoke>, ChangeResponse<SetupKey | null>, ChangeResponse<SetupKey | null>>(), )<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>(), removeSetupKey: createAction('REMOVE_SETUP_KEY')<string>(),
setSetupKey: createAction('SET_SETUP_KEY')<SetupKey>(), setSetupKey: createAction('SET_SETUP_KEY')<SetupKey>(),

View File

@@ -72,13 +72,15 @@ const deletedSetupKey = createReducer<DeleteResponse<string | null>, ActionTypes
.handleAction(actions.deleteSetupKey.request, () => initialState.deletedSetupKey) .handleAction(actions.deleteSetupKey.request, () => initialState.deletedSetupKey)
.handleAction(actions.deleteSetupKey.success, (store, action) => action.payload) .handleAction(actions.deleteSetupKey.success, (store, action) => action.payload)
.handleAction(actions.deleteSetupKey.failure, (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) const revokedSetupKey = createReducer<ChangeResponse<SetupKey | null>, ActionTypes>(initialState.revokedSetupKey)
.handleAction(actions.revokeSetupKey.request, () => initialState.revokedSetupKey) .handleAction(actions.revokeSetupKey.request, () => initialState.revokedSetupKey)
.handleAction(actions.revokeSetupKey.success, (store, action) => action.payload) .handleAction(actions.revokeSetupKey.success, (store, action) => action.payload)
.handleAction(actions.revokeSetupKey.failure, (store, action) => action.payload) .handleAction(actions.revokeSetupKey.failure, (store, action) => action.payload)
.handleAction(actions.setRevokeSetupKey, (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) const createdSetupKey = createReducer<CreateResponse<SetupKey | null>, ActionTypes>(initialState.createdSetupKey)
.handleAction(actions.createSetupKey.request, () => initialState.createdSetupKey) .handleAction(actions.createSetupKey.request, () => initialState.createdSetupKey)

View File

@@ -3,18 +3,9 @@ import {ApiError, ApiResponse, ChangeResponse, CreateResponse, DeleteResponse} f
import {SetupKey, SetupKeyRevoke} from './types' import {SetupKey, SetupKeyRevoke} from './types'
import service from './service'; import service from './service';
import actions from './actions'; import actions from './actions';
import {take} from "lodash";
export function* getSetupKeys(action: ReturnType<typeof actions.getSetupKeys.request>): Generator { export function* getSetupKeys(action: ReturnType<typeof actions.getSetupKeys.request>): Generator {
try { 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 effect = yield call(service.getSetupKeys, action.payload);
const response = effect as ApiResponse<SetupKey[]>; const response = effect as ApiResponse<SetupKey[]>;
@@ -89,7 +80,7 @@ export function* deleteSetupKey(action: ReturnType<typeof actions.deleteSetupKey
} as DeleteResponse<string | null>)); } as DeleteResponse<string | null>));
const setupKeys = (yield select(state => state.setupKey.data)) as SetupKey[] 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) { } catch (err) {
yield put(actions.deleteSetupKey.failure({ yield put(actions.deleteSetupKey.failure({
loading: false, loading: false,
@@ -123,12 +114,12 @@ export function* revokeSetupKey(action: ReturnType<typeof actions.revokeSetupKey
} as ChangeResponse<SetupKey | null>)); } as ChangeResponse<SetupKey | null>));
const setupKeys = [...(yield select(state => state.setupKey.data)) as SetupKey[]] 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) { if (setupKey) {
setupKey.Revoked = response.body.Revoked setupKey.revoked = response.body.revoked
setupKey.Valid = response.body.Valid setupKey.valid = response.body.valid
setupKey.State = response.body.State setupKey.state = response.body.state
setupKey.Expires = response.body.Expires setupKey.expires = response.body.expires
} }
yield put(actions.getSetupKeys.success(setupKeys)); yield put(actions.getSetupKeys.success(setupKeys));
} catch (err) { } catch (err) {

View File

@@ -17,13 +17,13 @@ export default {
}, },
async revokeSetupKey(payload:RequestPayload<SetupKeyRevoke>): Promise<ApiResponse<SetupKey>> { async revokeSetupKey(payload:RequestPayload<SetupKeyRevoke>): Promise<ApiResponse<SetupKey>> {
return apiClient.put<SetupKey>( return apiClient.put<SetupKey>(
`/api/setup-keys/` + payload.payload.Id, `/api/setup-keys/` + payload.payload.id,
payload payload
); );
}, },
async renameSetupKey(payload:RequestPayload<any>): Promise<ApiResponse<SetupKey>> { async renameSetupKey(payload:RequestPayload<any>): Promise<ApiResponse<SetupKey>> {
return apiClient.put<SetupKey>( return apiClient.put<SetupKey>(
`/api/setup-keys/` + payload.payload.Id, `/api/setup-keys/` + payload.payload.id,
payload payload
); );
}, },

View File

@@ -1,23 +1,23 @@
export interface SetupKey { export interface SetupKey {
Expires: string; expires: string;
Id: string; id: string;
Key: string; key: string;
LastUsed: string; last_used: string;
Name: string; name: string;
Revoked: boolean; revoked: boolean;
State: string; state: string;
Type: string; type: string;
UsedTimes: number; used_times: number;
Valid: boolean; valid: boolean;
} }
export interface SetupKeyNew { export interface SetupKeyNew {
Id: string; id: string;
Name: string; name: string;
Type: string; type: string;
} }
export interface SetupKeyRevoke { export interface SetupKeyRevoke {
Id: string; id: string;
Revoked: boolean; revoked: boolean;
} }

View File

@@ -4,7 +4,7 @@ import {
Alert, Alert,
Button, Card, Button, Card,
Col, Dropdown, Input, Menu, message, Modal, Popover, Radio, RadioChangeEvent, Col, Dropdown, Input, Menu, message, Modal, Popover, Radio, RadioChangeEvent,
Row, Select, Space, Table, Tag, Row, Select, Space, Table, Tag, Tooltip,
Typography Typography
} from "antd"; } from "antd";
import {Container} from "../components/Container"; import {Container} from "../components/Container";
@@ -24,6 +24,8 @@ import AccessControlNew from "../components/AccessControlNew";
import {Group} from "../store/group/types"; import {Group} from "../store/group/types";
import {actions as setupKeyActions} from "../store/setup-key"; import {actions as setupKeyActions} from "../store/setup-key";
import AccessControlModalGroups from "../components/AccessControlModalGroups"; import AccessControlModalGroups from "../components/AccessControlModalGroups";
import TableSpin from "../components/Spin";
import tableSpin from "../components/Spin";
const { Title, Paragraph } = Typography; const { Title, Paragraph } = Typography;
const { Column } = Table; const { Column } = Table;
@@ -55,7 +57,7 @@ export const AccessControl = () => {
const [showTutorial, setShowTutorial] = useState(true) const [showTutorial, setShowTutorial] = useState(true)
const [textToSearch, setTextToSearch] = useState(''); const [textToSearch, setTextToSearch] = useState('');
const [optionAllEnable, setOptionAllEnable] = useState('all'); const [optionAllEnable, setOptionAllEnable] = useState('enabled');
const [pageSize, setPageSize] = useState(5); const [pageSize, setPageSize] = useState(5);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [dataTable, setDataTable] = useState([] as RuleDataTable[]); const [dataTable, setDataTable] = useState([] as RuleDataTable[]);
@@ -68,7 +70,7 @@ export const AccessControl = () => {
{label: "15", value: "15"} {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 = [ const itemsMenuAction = [
{ {
@@ -87,22 +89,22 @@ export const AccessControl = () => {
const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>) const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>)
const getSourceDestinationLabel = (data:Group[]):string => { 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 => { 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[] => { const transformDataTable = (d:Rule[]):RuleDataTable[] => {
return d.map(p => { return d.map(p => {
const sourceLabel = getSourceDestinationLabel(p.Source as Group[]) const sourceLabel = getSourceDestinationLabel(p.sources as Group[])
const destinationLabel = getSourceDestinationLabel(p.Destination as Group[]) const destinationLabel = getSourceDestinationLabel(p.destinations as Group[])
return { return {
key: p.ID, ...p, key: p.id, ...p,
sourceCount: p.Source?.length, sourceCount: p.sources?.length,
sourceLabel, sourceLabel,
destinationCount: p.Destination?.length, destinationCount: p.destinations?.length,
destinationLabel destinationLabel
} as RuleDataTable } as RuleDataTable
}) })
@@ -115,7 +117,7 @@ export const AccessControl = () => {
useEffect(() => { useEffect(() => {
setShowTutorial(isShowTutorial(rules)) setShowTutorial(isShowTutorial(rules))
setDataTable(sortBy(transformDataTable(rules), "Name")) setDataTable(sortBy(transformDataTable(filterDataTable()), "name"))
}, [rules]) }, [rules])
useEffect(() => { useEffect(() => {
@@ -127,14 +129,16 @@ export const AccessControl = () => {
const saveKey = 'saving'; const saveKey = 'saving';
useEffect(() => { useEffect(() => {
if (savedRule.loading) { 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) { } else if (savedRule.success) {
message.success({ content: 'Rule saved with success!', key: saveKey, duration: 2, style: styleNotification }); message.success({ content: 'Rule has been successfully updated.', key: saveKey, duration: 2, style: styleNotification });
dispatch(ruleActions.setSetupNewRuleVisible(false)); dispatch(ruleActions.setSetupNewRuleVisible(false))
dispatch(ruleActions.setSavedRule({ ...savedRule, success: false })); dispatch(ruleActions.setSavedRule({ ...savedRule, success: false }))
dispatch(ruleActions.resetSavedRule(null))
} else if (savedRule.error) { } else if (savedRule.error) {
message.error({ content: 'Error! Something wrong to create key.', key: saveKey, duration: 2, style: styleNotification }); 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.setSavedRule({ ...savedRule, error: null }))
dispatch(ruleActions.resetSavedRule(null))
} }
}, [savedRule]) }, [savedRule])
@@ -142,11 +146,13 @@ export const AccessControl = () => {
useEffect(() => { useEffect(() => {
const style = { marginTop: 85 } const style = { marginTop: 85 }
if (deletedRule.loading) { if (deletedRule.loading) {
message.loading({ content: 'Deleting...', key: deleteKey, style }); message.loading({ content: 'Deleting...', key: deleteKey, style })
} else if (deletedRule.success) { } 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) { } 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]) }, [deletedRule])
@@ -174,14 +180,14 @@ export const AccessControl = () => {
content: <Space direction="vertical" size="small"> content: <Space direction="vertical" size="small">
{ruleToAction && {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> <Paragraph>Are you sure you want to delete peer from your account?</Paragraph>
</> </>
} }
</Space>, </Space>,
okType: 'danger', okType: 'danger',
onOk() { onOk() {
dispatch(ruleActions.deleteRule.request({getAccessTokenSilently, payload: ruleToAction?.ID || ''})); dispatch(ruleActions.deleteRule.request({getAccessTokenSilently, payload: ruleToAction?.id || ''}));
}, },
onCancel() { onCancel() {
setRuleToAction(null); setRuleToAction(null);
@@ -196,14 +202,14 @@ export const AccessControl = () => {
content: <Space direction="vertical" size="small"> content: <Space direction="vertical" size="small">
{ruleToAction && {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> <Paragraph>Are you sure you want to deactivate peer from your account?</Paragraph>
</> </>
} }
</Space>, </Space>,
okType: 'danger', okType: 'danger',
onOk() { onOk() {
//dispatch(ruleActions.deleteRule.request({getAccessTokenSilently, payload: ruleToAction?.ID || ''})); //dispatch(ruleActions.deleteRule.request({getAccessTokenSilently, payload: ruleToAction?.id || ''}));
}, },
onCancel() { onCancel() {
setRuleToAction(null); setRuleToAction(null);
@@ -214,32 +220,49 @@ export const AccessControl = () => {
const filterDataTable = ():Rule[] => { const filterDataTable = ():Rule[] => {
const t = textToSearch.toLowerCase().trim() const t = textToSearch.toLowerCase().trim()
let f:Rule[] = filter(rules, (f:Rule) => 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[] ) as Rule[]
// if (optionAllEnabled === "enabled") { if (optionAllEnable !== "all") {
// f = filter(rules, (f:Rule) => f.) f = filter(f, (f:Rule) => !f.disabled)
// } }
return f return f
} }
const onClickAddNewRule = () => { const onClickAddNewRule = () => {
dispatch(ruleActions.setSetupNewRuleVisible(true)); dispatch(ruleActions.setSetupNewRuleVisible(true));
dispatch(ruleActions.setRule({ dispatch(ruleActions.setRule({
Name: '', name: '',
Source: [], description: '',
Destination: [], sources: [],
Flow: 'bidirect' destinations: [],
flow: 'bidirect',
disabled: false
} as Rule)) } as Rule))
} }
const onClickViewRule = () => { const onClickViewRule = () => {
dispatch(ruleActions.setSetupNewRuleVisible(true)); dispatch(ruleActions.setSetupNewRuleVisible(true));
dispatch(ruleActions.setRule({ dispatch(ruleActions.setRule({
ID: ruleToAction?.ID || null, id: ruleToAction?.id || null,
Name: ruleToAction?.Name, name: ruleToAction?.name,
Source: ruleToAction?.Source, description: ruleToAction?.description,
Destination: ruleToAction?.Destination, sources: ruleToAction?.sources,
Flow: ruleToAction?.Flow 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)) } as Rule))
} }
@@ -251,17 +274,17 @@ export const AccessControl = () => {
}) })
} }
const renderPopoverGroups = (label: string, groups:Group[] | string[] | null) => { const renderPopoverGroups = (label: string, groups:Group[] | string[] | null, rule: RuleDataTable) => {
const content = groups?.map(g => { const content = groups?.map((g, i) => {
const _g = g as Group 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 ( return (
<div> <div key={i}>
<Tag <Tag
color="blue" color="blue"
style={{ marginRight: 3 }} style={{ marginRight: 3 }}
> >
<strong>{_g.Name}</strong> <strong>{_g.name}</strong>
</Tag> </Tag>
<span style={{fontSize: ".85em"}}>{peersCount}</span> <span style={{fontSize: ".85em"}}>{peersCount}</span>
</div> </div>
@@ -269,7 +292,7 @@ export const AccessControl = () => {
}) })
return ( return (
<Popover content={<Space direction="vertical">{content}</Space>} title={null}> <Popover content={<Space direction="vertical">{content}</Space>} title={null}>
<Button type="link">{label}</Button> <Button type="link" onClick={() => setRuleAndView(rule)}>{label}</Button>
</Popover> </Popover>
) )
} }
@@ -280,7 +303,7 @@ export const AccessControl = () => {
<Row> <Row>
<Col span={24}> <Col span={24}>
<Title level={4}>Access Control</Title> <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' }}> <Space direction="vertical" size="large" style={{ display: 'flex' }}>
<Row gutter={[16, 24]}> <Row gutter={[16, 24]}>
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}> <Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
@@ -288,13 +311,13 @@ export const AccessControl = () => {
</Col> </Col>
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}> <Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
<Space size="middle"> <Space size="middle">
{/*<Radio.Group <Radio.Group
options={optionsAllEnabled} options={optionsAllEnabled}
onChange={onChangeAllEnabled} onChange={onChangeAllEnabled}
value={optionAllEnable} value={optionAllEnable}
optionType="button" optionType="button"
buttonStyle="solid" buttonStyle="solid"
/>*/} />
<Select value={pageSize.toString()} options={pageSizeOptions} onChange={onChangePageSize} className="select-rows-per-page-en"/> <Select value={pageSize.toString()} options={pageSizeOptions} onChange={onChangePageSize} className="select-rows-per-page-en"/>
</Space> </Space>
</Col> </Col>
@@ -314,7 +337,6 @@ export const AccessControl = () => {
{failed && {failed &&
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/> <Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
} }
{loading && <Loading/>}
<Card bodyStyle={{padding: 0}}> <Card bodyStyle={{padding: 0}}>
<Table <Table
pagination={{ pagination={{
@@ -325,20 +347,34 @@ export const AccessControl = () => {
setCurrentPage(page) 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} showSorterTooltip={false}
scroll={{x: true}} scroll={{x: true}}
loading={tableSpin(loading)}
dataSource={dataTable}> dataSource={dataTable}>
<Column title="Name" dataIndex="Name" <Column title="Name" dataIndex="name"
onFilter={(value: string | number | boolean, record) => (record as any).Name.includes(value)} onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
sorter={(a, b) => ((a as any).Name.localeCompare((b as any).Name))} /> 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" <Column title="Sources" dataIndex="sourceLabel"
render={(text, record:RuleDataTable, index) => { render={(text, record:RuleDataTable, index) => {
//return <Button type="link" onClick={() => toggleModalGroups(`${record.Name} - Sources`, record.Source, true)}>{text}</Button> //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) => { render={(text, record:RuleDataTable, index) => {
const s = {minWidth: 50, textAlign: "center"} as React.CSSProperties const s = {minWidth: 50, textAlign: "center"} as React.CSSProperties
if (text === "bidirect") if (text === "bidirect")
@@ -353,13 +389,13 @@ export const AccessControl = () => {
/> />
<Column title="Destinations" dataIndex="destinationLabel" <Column title="Destinations" dataIndex="destinationLabel"
render={(text, record:RuleDataTable, index) => { render={(text, record:RuleDataTable, index) => {
//return <Button type="link" onClick={() => toggleModalGroups(`${record.Name} - Destinations`, record.Destination, true)}>{text}</Button> //return <Button type="link" onClick={() => toggleModalGroups(`${record.name} - Destinations`, record.destinations, true)}>{text}</Button>
return renderPopoverGroups(text, record.Destination) return renderPopoverGroups(text, record.destinations,record as RuleDataTable)
}} }}
/> />
<Column title="" align="center" <Column title="" align="center"
render={(text, record, index) => { 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"]} return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
onVisibleChange={visible => { onVisibleChange={visible => {
if (visible) setRuleToAction(record as RuleDataTable) if (visible) setRuleToAction(record as RuleDataTable)
@@ -370,11 +406,6 @@ export const AccessControl = () => {
{showTutorial && {showTutorial &&
<Space direction="vertical" size="small" align="center" <Space direction="vertical" size="small" align="center"
style={{display: 'flex', padding: '45px 15px'}}> 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> <Button type="link" onClick={onClickAddNewRule}>Add new access rule</Button>
</Space> </Space>
} }

View File

@@ -20,21 +20,25 @@ import {
RadioChangeEvent, RadioChangeEvent,
Dropdown, Dropdown,
Menu, Menu,
Alert, Select, Modal, Button, message Alert, Select, Modal, Button, message, Popover, SpinProps, Spin
} from "antd"; } from "antd";
import {Peer} from "../store/peer/types"; import {Peer} from "../store/peer/types";
import {filter} from "lodash" import {filter} from "lodash"
import {formatOS, timeAgo} from "../utils/common"; import {formatOS, timeAgo} from "../utils/common";
import {ExclamationCircleOutlined} from "@ant-design/icons"; import {ExclamationCircleOutlined} from "@ant-design/icons";
import ButtonCopyMessage from "../components/ButtonCopyMessage"; 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 { Title, Paragraph } = Typography;
const { Column } = Table; const { Column } = Table;
const { confirm } = Modal; const { confirm } = Modal;
interface PeerDataTable extends Peer { interface PeerDataTable extends Peer {
key: string key: string;
groups: Group[];
groupsCount: number;
} }
export const Peers = () => { export const Peers = () => {
@@ -47,6 +51,7 @@ export const Peers = () => {
const deletedPeer = useSelector((state: RootState) => state.peer.deletedPeer); const deletedPeer = useSelector((state: RootState) => state.peer.deletedPeer);
const groups = useSelector((state: RootState) => state.group.data); const groups = useSelector((state: RootState) => state.group.data);
const loadingGroups = useSelector((state: RootState) => state.group.loading); const loadingGroups = useSelector((state: RootState) => state.group.loading);
const savedGroups = useSelector((state: RootState) => state.peer.savedGroups);
const [textToSearch, setTextToSearch] = useState(''); const [textToSearch, setTextToSearch] = useState('');
const [optionOnOff, setOptionOnOff] = useState('all'); const [optionOnOff, setOptionOnOff] = useState('all');
@@ -60,7 +65,7 @@ export const Peers = () => {
{label: "15", value: "15"} {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 = [ const itemsMenuAction = [
{ {
@@ -71,22 +76,28 @@ export const Peers = () => {
const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>) const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>)
const transformDataTable = (d:Peer[]):PeerDataTable[] => { 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(() => { useEffect(() => {
dispatch(peerActions.getPeers.request({getAccessTokenSilently, payload: null})); dispatch(peerActions.getPeers.request({getAccessTokenSilently, payload: null}));
dispatch(groupActions.getGroups.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(() => { useEffect(() => {
setDataTable(transformDataTable(peers)) setDataTable(transformDataTable(peers))
}, [peers]) }, [peers, groups])
useEffect(() => { useEffect(() => {
setDataTable(transformDataTable(filterDataTable())) setDataTable(transformDataTable(filterDataTable()))
@@ -99,18 +110,35 @@ export const Peers = () => {
message.loading({ content: 'Deleting...', key: deleteKey, style }); message.loading({ content: 'Deleting...', key: deleteKey, style });
} else if (deletedPeer.success) { } else if (deletedPeer.success) {
message.success({ content: 'Peer has been successfully removed.', key: deleteKey, duration: 2, style }); message.success({ content: 'Peer has been successfully removed.', key: deleteKey, duration: 2, style });
dispatch(peerActions.resetDeletedPeer(null))
} else if (deletedPeer.error) { } else if (deletedPeer.error) {
message.error({ content: 'Failed to delete peer. You might not have enough permissions.', key: deleteKey, duration: 2, style }); message.error({ content: 'Failed to delete peer. You might not have enough permissions.', key: deleteKey, duration: 2, style });
dispatch(peerActions.resetDeletedPeer(null))
} }
}, [deletedPeer]) }, [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 filterDataTable = ():Peer[] => {
const t = textToSearch.toLowerCase().trim() const t = textToSearch.toLowerCase().trim()
let f:Peer[] = filter(peers, (f:Peer) => 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[] ) as Peer[]
if (optionOnOff === "on") { if (optionOnOff === "on") {
f = filter(peers, (f:Peer) => f.Connected) f = filter(peers, (f:Peer) => f.connected)
} }
return f return f
} }
@@ -139,14 +167,14 @@ export const Peers = () => {
content: <Space direction="vertical" size="small"> content: <Space direction="vertical" size="small">
{peerToAction && {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> <Paragraph>Are you sure you want to delete peer from your account?</Paragraph>
</> </>
} }
</Space>, </Space>,
okType: 'danger', okType: 'danger',
onOk() { onOk() {
dispatch(peerActions.deletedPeer.request({getAccessTokenSilently, payload: peerToAction ? peerToAction.IP : ''})); dispatch(peerActions.deletedPeer.request({getAccessTokenSilently, payload: peerToAction ? peerToAction.ip : ''}));
}, },
onCancel() { onCancel() {
setPeerToAction(null); 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 ( return (
<Container style={{paddingTop: "40px"}}> <>
<Row> <Container style={{paddingTop: "40px"}}>
<Col span={24}> <Row>
<Title level={4}>Peers</Title> <Col span={24}>
<Paragraph>A list of all the machines in your account including their name, IP and status.</Paragraph> <Title level={4}>Peers</Title>
<Space direction="vertical" size="large" style={{ display: 'flex' }}> <Paragraph>A list of all the machines in your account including their name, IP and status.</Paragraph>
<Row gutter={[16, 24]}> <Space direction="vertical" size="large" style={{ display: 'flex' }}>
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}> <Row gutter={[16, 24]}>
{/*<Input.Search allowClear value={textToSearch} onPressEnter={searchDataTable} onSearch={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />*/} <Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
<Input allowClear value={textToSearch} onPressEnter={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} /> {/*<Input.Search allowClear value={textToSearch} onPressEnter={searchDataTable} onSearch={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />*/}
</Col> <Input allowClear value={textToSearch} onPressEnter={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}> </Col>
<Space size="middle"> <Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
<Radio.Group <Space size="middle">
options={optionsOnOff} <Radio.Group
onChange={onChangeOnOff} options={optionsOnOff}
value={optionOnOff} onChange={onChangeOnOff}
optionType="button" value={optionOnOff}
buttonStyle="solid" 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"/> <Column title="Status" dataIndex="connected"
</Space> render={(text, record, index) => {
</Col> return text ? <Tag color="green">online</Tag> : <Tag color="red">offline</Tag>
<Col xs={24} }}
sm={24} />
md={5} <Column title="Groups" dataIndex="groupsCount"
lg={5} render={(text, record:PeerDataTable, index) => {
xl={5} return renderPopoverGroups(text, record.groups, record)
xxl={5} span={5}> }}
<Row justify="end"> />
<Col> <Column title="LastSeen" dataIndex="last_seen"
<Link to="/add-peer" className="ant-btn ant-btn-primary ant-btn-block">Add Peer</Link> render={(text, record, index) => {
</Col> return (record as PeerDataTable).connected ? 'just now' : timeAgo(text)
</Row> }}
</Col> />
</Row> <Column title="OS" dataIndex="os"
{failed && render={(text, record, index) => {
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/> return formatOS(text)
} }}
{loading && <Loading/>} />
<Card bodyStyle={{padding: 0}}> <Column title="Version" dataIndex="version" />
<Table <Column title="" align="center"
pagination={{pageSize, showSizeChanger: false, showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} peers`)}} render={(text, record, index) => {
className="card-table" return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
showSorterTooltip={false} onVisibleChange={visible => {
scroll={{x: true}} if (visible) setPeerToAction(record as PeerDataTable)
dataSource={dataTable}> }}></Dropdown.Button>
<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))} /> </Table>
<Column title="IP" dataIndex="IP" </Card>
sorter={(a, b) => { </Space>
const _a = (a as any).IP.split('.') </Col>
const _b = (b as any).IP.split('.') </Row>
const a_s = _a.map((i:any) => i.padStart(3, '0')).join() </Container>
const b_s = _b.map((i:any) => i.padStart(3, '0')).join() <PeerGroupsUpdate/>
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>
) )
} }

View File

@@ -26,6 +26,8 @@ import {copyToClipboard, formatDate, formatOS, timeAgo} from "../utils/common";
import {ExclamationCircleOutlined} from "@ant-design/icons"; import {ExclamationCircleOutlined} from "@ant-design/icons";
import SetupKeyNew from "../components/SetupKeyNew"; import SetupKeyNew from "../components/SetupKeyNew";
import ButtonCopyMessage from "../components/ButtonCopyMessage"; import ButtonCopyMessage from "../components/ButtonCopyMessage";
import TableSpin from "../components/Spin";
import tableSpin from "../components/Spin";
const { Title, Text, Paragraph } = Typography; const { Title, Text, Paragraph } = Typography;
const { Column } = Table; const { Column } = Table;
@@ -75,7 +77,7 @@ export const SetupKeys = () => {
const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>) const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>)
const transformDataTable = (d:SetupKey[]):SetupKeyDataTable[] => { const transformDataTable = (d:SetupKey[]):SetupKeyDataTable[] => {
return d.map(p => ({ key: p.Id, ...p } as SetupKeyDataTable)) return d.map(p => ({ ...p } as SetupKeyDataTable))
} }
useEffect(() => { useEffect(() => {
@@ -96,23 +98,25 @@ export const SetupKeys = () => {
message.loading({ content: 'Deleting...', key: deleteKey, style: styleNotification }); message.loading({ content: 'Deleting...', key: deleteKey, style: styleNotification });
} else if (deletedSetupKey.success) { } else if (deletedSetupKey.success) {
message.success({ content: 'Setup key has been successfully removed.', key: deleteKey, duration: 2, style: styleNotification }); 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) { } else if (deletedSetupKey.error) {
message.error({ content: 'Failed to delete setup key. You might not have enough permissions.', key: deleteKey, duration: 2, style: styleNotification }); 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]) }, [deletedSetupKey])
const revokeKey = 'creating'; const revokeKey = 'revoking';
useEffect(() => { useEffect(() => {
if (revokedSetupKey.loading) { 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) { } else if (revokedSetupKey.success) {
message.success({ content: 'Setup key has been successfully revoked.', key: revokeKey, duration: 2, style: styleNotification }); 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) { } else if (revokedSetupKey.error) {
message.error({ content: 'Failed to revoke setup key. You might not have enough permissions.', key: revokeKey, duration: 2, style: styleNotification }); 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]) }, [revokedSetupKey])
@@ -134,10 +138,10 @@ export const SetupKeys = () => {
const t = textToSearch.toLowerCase().trim() const t = textToSearch.toLowerCase().trim()
let f:SetupKey[] = [...setupKeys] let f:SetupKey[] = [...setupKeys]
if (optionValidAll === "valid") { 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 = 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[] ) as SetupKey[]
return f return f
} }
@@ -166,14 +170,14 @@ export const SetupKeys = () => {
content: <Space direction="vertical" size="small"> content: <Space direction="vertical" size="small">
{setupKeyToAction && {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> <Paragraph>Are you sure you want to delete key?</Paragraph>
</> </>
} }
</Space>, </Space>,
okType: 'danger', okType: 'danger',
onOk() { onOk() {
dispatch(setupKeyActions.deleteSetupKey.request({getAccessTokenSilently, payload: setupKeyToAction ? setupKeyToAction.Id : ''})); dispatch(setupKeyActions.deleteSetupKey.request({getAccessTokenSilently, payload: setupKeyToAction ? setupKeyToAction.id : ''}));
}, },
onCancel() { onCancel() {
setSetupKeyToAction(null); setSetupKeyToAction(null);
@@ -188,14 +192,14 @@ export const SetupKeys = () => {
content: <Space direction="vertical" size="small"> content: <Space direction="vertical" size="small">
{setupKeyToAction && {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> <Paragraph>Are you sure you want to revoke key?</Paragraph>
</> </>
} }
</Space>, </Space>,
okType: 'danger', okType: 'danger',
onOk() { 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() { onCancel() {
setSetupKeyToAction(null); setSetupKeyToAction(null);
@@ -206,8 +210,8 @@ export const SetupKeys = () => {
const onClickAddNewSetupKey = () => { const onClickAddNewSetupKey = () => {
dispatch(setupKeyActions.setSetupNewKeyVisible(true)); dispatch(setupKeyActions.setSetupNewKeyVisible(true));
dispatch(setupKeyActions.setSetupKey({ dispatch(setupKeyActions.setSetupKey({
Name: '', name: '',
Type: 'reusable' type: 'reusable'
} as SetupKey)) } as SetupKey))
} }
@@ -252,49 +256,51 @@ export const SetupKeys = () => {
{failed && {failed &&
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/> <Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
} }
{loading && <Loading/>}
<Card bodyStyle={{padding: 0}}> <Card bodyStyle={{padding: 0}}>
<Table <Table
pagination={{pageSize, showSizeChanger: false, showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} setup keys`)}} pagination={{pageSize, showSizeChanger: false, showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} setup keys`)}}
className="card-table" className="card-table"
showSorterTooltip={false} showSorterTooltip={false}
scroll={{x: true}} scroll={{x: true}}
loading={tableSpin(loading)}
dataSource={dataTable}> dataSource={dataTable}>
<Column title="Name" dataIndex="Name" <Column title="Name" dataIndex="name"
onFilter={(value: string | number | boolean, record) => (record as any).Name.includes(value)} onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
sorter={(a, b) => ((a as any).Name.localeCompare((b as any).Name))} 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) => { render={(text, record, index) => {
return (text === 'valid') ? <Tag color="green">{text}</Tag> : <Tag color="red">{text}</Tag> 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" <Column title="Type" dataIndex="type"
onFilter={(value: string | number | boolean, record) => (record as any).Type.includes(value)} onFilter={(value: string | number | boolean, record) => (record as any).type.includes(value)}
sorter={(a, b) => ((a as any).Type.localeCompare((b as any).Type))} sorter={(a, b) => ((a as any).type.localeCompare((b as any).type))}
/> />
<Column title="Key" dataIndex="Key" <Column title="Key" dataIndex="key"
onFilter={(value: string | number | boolean, record) => (record as any).Key.includes(value)} onFilter={(value: string | number | boolean, record) => (record as any).key.includes(value)}
sorter={(a, b) => ((a as any).Key.localeCompare((b as any).Key))} sorter={(a, b) => ((a as any).key.localeCompare((b as any).key))}
render={(text, record, index) => { render={(text, record, index) => {
return <ButtonCopyMessage keyMessage={(record as SetupKeyDataTable).key} text={text} messageText={`Key copied!`} styleNotification={{}}/> 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) => { 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" <Column title="Used Times" dataIndex="used_times"
sorter={(a, b) => ((a as any).Type.localeCompare((b as any).Type))} 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) => { render={(text, record, index) => {
return formatDate(text) return formatDate(text)
}} }}
@@ -302,7 +308,7 @@ export const SetupKeys = () => {
<Column title="" align="center" <Column title="" align="center"
render={(text, record, index) => { render={(text, record, index) => {
return !(record as SetupKeyDataTable).Revoked ? ( return !(record as SetupKeyDataTable).revoked ? (
<Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]} <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
onVisibleChange={visible => { onVisibleChange={visible => {
if (visible) setSetupKeyToAction(record as SetupKeyDataTable) if (visible) setSetupKeyToAction(record as SetupKeyDataTable)

View File

@@ -16,6 +16,7 @@ import {
import { User } from "../store/user/types"; import { User } from "../store/user/types";
import {filter} from "lodash"; import {filter} from "lodash";
import {formatOS, timeAgo} from "../utils/common"; import {formatOS, timeAgo} from "../utils/common";
import tableSpin from "../components/Spin";
const { Title, Paragraph } = Typography; const { Title, Paragraph } = Typography;
const { Column } = Table; const { Column } = Table;
@@ -97,17 +98,18 @@ export const Activity = () => {
{failed && {failed &&
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/> <Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
} }
{loading && <Loading/>}
<Card bodyStyle={{padding: 0}}> <Card bodyStyle={{padding: 0}}>
<Table <Table
pagination={{pageSize, showSizeChanger: false, showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} users`)}} pagination={{pageSize, showSizeChanger: false, showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} users`)}}
className="card-table" className="card-table"
showSorterTooltip={false} showSorterTooltip={false}
scroll={{x: true}} scroll={{x: true}}
loading={tableSpin(loading)}
dataSource={dataTable}> dataSource={dataTable}>
<Column title="Email" dataIndex="email" <Column title="Email" dataIndex="email"
onFilter={(value: string | number | boolean, record) => (record as any).email.includes(value)} onFilter={(value: string | number | boolean, record) => (record as any).email.includes(value)}
sorter={(a, b) => ((a as any).email.localeCompare((b as any).email))} sorter={(a, b) => ((a as any).email.localeCompare((b as any).email))}
defaultSortOrder='ascend'
render={(text:string | null, record, index) => { render={(text:string | null, record, index) => {
return (text && text.trim() !== "") ? text : (record as User).id return (text && text.trim() !== "") ? text : (record as User).id
}} }}