Refactor with ant design and TypeScript (#51)

Refactoring UI using ant design

This will allow us to move forward faster because of its popularity

We also rewrote the code in TypeScript, 
we believe this is also a major step moving forward 
with the project as it brings more struct and clear/clean code. 

Co-authored-by: Raphael Oliveira <raphael.oliveira@dataontabs.com>
Co-authored-by: braginini <bangvalo@gmail.com>
This commit is contained in:
Maycon Santos
2022-06-01 22:44:06 +02:00
committed by GitHub
parent a6ec8ed865
commit 009933c0e2
103 changed files with 5348 additions and 19969 deletions

View File

@@ -1,16 +1,16 @@
# netbird dashboard
# NetBird dashboard
### Big News! Wiretrustee becomes `netbird`. [See details](https://blog.netbird.io/wiretrustee-becomes-netbird).
### Big News! Wiretrustee becomes `NetBird`. [See details](https://blog.netbird.io/wiretrustee-becomes-netbird).
\
This project is the UI for netbird's management service.
This project is the UI for NetBird's management service.
**Hosted demo version:** https://app.netbird.io/
See [netbird repo](https://github.com/netbirdio/netbird)
See [NetBird repo](https://github.com/netbirdio/netbird)
## Why UI dashboard is needed?
The purpose of this project is simple - make it easy to manage VPN built with [netbird](https://github.com/netbirdio/netbird).
The purpose of this project is simple - make it easy to manage VPN built with [NetBird](https://github.com/netbirdio/netbird).
The dashboard makes it possible to:
- track the status of your peers
- remove Peers

17799
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,26 +3,45 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^4.7.0",
"@auth0/auth0-react": "^1.6.0",
"@headlessui/react": "^1.5.0",
"@heroicons/react": "^1.0.4",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^27.5.1",
"@types/lodash": "^4.14.182",
"@types/node": "^17.0.35",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.5",
"@types/react-redux": "^7.1.24",
"@types/react-router-dom": "^5.3.3",
"@types/styled-components": "^5.1.25",
"antd": "^4.20.6",
"autoprefixer": "^10.4.4",
"axios": "^0.27.2",
"heroicons": "^1.0.6",
"highlight.js": "^11.5.1",
"highlight.js": "^11.2.0",
"history": "^5.0.1",
"lodash": "^4.17.21",
"postcss": "^8.4.12",
"prop-types": "^15.7.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-highlight": "^0.14.0",
"react-redux": "^8.0.2",
"react-router-dom": "^5.2.0",
"react-scripts": "^5.0.1",
"react-table": "^7.7.0",
"redux": "^4.2.0",
"redux-devtools-extension": "^2.13.9",
"redux-saga": "^1.1.3",
"styled-components": "^5.3.5",
"tailwindcss": "^3.0.23",
"web-vitals": "^0.2.4",
"nth-check": ">=2.0.1"
"typesafe-actions": "^5.1.0",
"typescript": "^4.6.4",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
@@ -47,5 +66,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react-highlight": "^0.12.5"
}
}

View File

@@ -7,13 +7,13 @@
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Netbird Management Dashboard"
content="NetBird Management Dashboard"
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Netbird</title>
<title>NetBird</title>
</head>
<body>
<noscript>Netbird Management Dashboard.</noscript>
<noscript>NetBird Management Dashboard.</noscript>
<div id="root"></div>
</body>
</html>

View File

@@ -1,90 +0,0 @@
import React, {useEffect, useState} from 'react';
import Navbar from './components/Navbar';
import {Redirect, Route, Switch} from 'react-router-dom';
import {Peers} from './views/Peers';
import {Users} from './views/Users';
import Footer from './components/Footer';
import {useAuth0} from "@auth0/auth0-react";
import Loading from "./components/Loading";
import SetupKeys from "./views/SetupKeys";
import AddPeer from "./views/AddPeer";
import AccessControl from "./views/AccessControl";
import Activity from "./views/Activity";
import Banner from "./components/Banner";
function App() {
const {
isLoading,
isAuthenticated,
loginWithRedirect,
error
} = useAuth0();
const [isOpen, setIsOpen] = useState(false);
const toggle = () => {
setIsOpen(!isOpen);
};
useEffect(() => {
const hideMenu = () => {
if (window.innerWidth > 768 && isOpen) {
setIsOpen(false);
console.log('i resized');
}
};
window.addEventListener('resize', hideMenu);
return () => {
window.removeEventListener('resize', hideMenu);
};
});
if (error) {
return <div>Oops... {error.message}</div>;
}
if (isLoading) {
return <Loading/>;
}
if (!isAuthenticated) {
loginWithRedirect({})
}
return (
isAuthenticated && (
<>
{/*<div className='h-screen flex justify-center items-center bg-green-400'>*/}
<Banner/>
<Navbar toggle={toggle}/>
<div className="min-h-screen bg-gray-50">
<Switch>
<Route
exact
path="/"
render={() => {
return (
<Redirect to="/peers"/>
)
}}
/>
<Route path='/peers' exact component={Peers}/>
<Route path="/add-peer" component={AddPeer}/>
<Route path="/setup-keys" component={SetupKeys}/>
<Route path="/users" component={Users}/>
<Route path="/acls" component={AccessControl}/>
<Route path="/activity" component={Activity}/>
</Switch>
</div>
<Footer/>
</>
)
);
}
export default App;

View File

@@ -1,3 +1,4 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

104
src/App.tsx Normal file
View File

@@ -0,0 +1,104 @@
import React, {useEffect, useState} from 'react';
import {Provider} from "react-redux";
import {Redirect, Route, Switch} from 'react-router-dom';
import {useAuth0} from "@auth0/auth0-react";
import Navbar from './components/Navbar';
import Peers from './views/Peers';
import FooterComponent from './components/FooterComponent';
import Loading from "./components/Loading";
import SetupKeys from "./views/SetupKeys";
import AddPeer from "./views/AddPeer";
import AccessControl from "./views/AccessControl";
import Activity from "./views/Activity";
import Users from './views/Users';
import Banner from "./components/Banner";
import {store} from "./store";
import {Col, Layout, Row} from 'antd';
import { Container } from "./components/Container";
const { Header, Content } = Layout;
function App() {
const {
isLoading,
isAuthenticated,
loginWithRedirect,
error
} = useAuth0();
const [isOpen, setIsOpen] = useState(false);
const toggle = () => {
setIsOpen(!isOpen);
};
useEffect(() => {
const hideMenu = () => {
if (window.innerWidth > 768 && isOpen) {
setIsOpen(false);
console.log('i resized');
}
};
window.addEventListener('resize', hideMenu);
return () => {
window.removeEventListener('resize', hideMenu);
};
});
if (error) {
return <div>Oops... {error.message}</div>;
}
if (isLoading) {
return <Loading padding="3em" width="50px" height="50px"/>;
}
if (!isAuthenticated) {
loginWithRedirect({})
}
return (
<Provider store={store}>
{ isAuthenticated &&
<Layout>
<Banner/>
<Header className="header" style={{display: "flex", flexDirection: "column", justifyContent: "space-around", alignContent: "center"}}>
<Row justify="space-around" align="middle">
<Col span={24}>
<Container>
<Navbar/>
</Container>
</Col>
</Row>
</Header>
<Content>
<Switch>
<Route
exact
path="/"
render={() => {
return (
<Redirect to="/peers"/>
)
}}
/>
<Route path='/peers' exact component={Peers}/>
<Route path="/add-peer" component={AddPeer}/>
<Route path="/setup-keys" component={SetupKeys}/>
{/*<Route path="/acls" component={AccessControl}/>
<Route path="/activity" component={Activity}/>*/}
<Route path="/users" component={Users}/>
</Switch>
</Content>
<FooterComponent/>
</Layout>
}
</Provider>
);
}
export default App;

View File

@@ -1,71 +0,0 @@
import {getConfig} from "../config";
const {apiOrigin} = getConfig();
export const callApi = async (method, headers, body, getAccessTokenSilently, endpoint) => {
const token = await getAccessTokenSilently();
if (!headers) {
headers = {}
}
headers.Authorization = `Bearer ${token}`
const requestOptions = {
method: method,
headers: headers,
body: body
};
const response = await fetch(`${apiOrigin}${endpoint}`, requestOptions);
return await response.json();
};
export const getUsers = async (getAccessTokenSilently) => {
return callApi("GET", {}, null, getAccessTokenSilently, "/api/users")
}
export const getSetupKeys = async (getAccessTokenSilently) => {
return callApi("GET", {}, null, getAccessTokenSilently, "/api/setup-keys")
}
export const revokeSetupKey = async (getAccessTokenSilently, keyId) => {
return callApi(
"PUT",
{'Content-Type': 'application/json'},
JSON.stringify({Revoked: true}),
getAccessTokenSilently,
"/api/setup-keys/" + keyId)
}
export const renameSetupKey = async (getAccessTokenSilently, keyId, newName) => {
return callApi(
"PUT",
{'Content-Type': 'application/json'},
JSON.stringify({Name: newName}),
getAccessTokenSilently,
"/api/setup-keys/" + keyId)
}
export const createSetupKey = async (getAccessTokenSilently, name, type, expiresIn) => {
return callApi(
"POST",
{'Content-Type': 'application/json'},
JSON.stringify({Name: name, Type: type, ExpiresIn: expiresIn}, (key, value) => {
if (value !== null) return value
}),
getAccessTokenSilently,
"/api/setup-keys")
}
export const getPeers = async (getAccessTokenSilently) => {
return callApi("GET", {}, null, getAccessTokenSilently, "/api/peers")
}
export const deletePeer = async (getAccessTokenSilently, peerId) => {
return callApi(
"DELETE",
{},
null,
getAccessTokenSilently,
"/api/peers/" + peerId)
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 454 KiB

3
src/assets/direct_bi.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="29.435" height="7.636" viewBox="0 0 29.435 7.636">
<path id="direct_bi" d="M3.64,8.158-.178,4.34,3.64.522,4.3,1.17l-2.7,2.7h25.89l-2.7-2.7.656-.648L29.257,4.34,27.213,6.384,25.439,8.158,24.783,7.5l2.7-2.693H1.595L4.3,7.5Z" transform="translate(0.178 -0.522)" fill="#1e429f"/>
</svg>

After

Width:  |  Height:  |  Size: 332 B

3
src/assets/direct_in.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="29.497" height="7.636" viewBox="0 0 29.497 7.636">
<path id="direct_out" d="M4.728,8l.656-.656-2.7-2.693H30.407V3.713H2.683l2.7-2.7L4.728.364.91,4.182Z" transform="translate(-0.91 -0.364)" fill="#03543f"/>
</svg>

After

Width:  |  Height:  |  Size: 262 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="29.497" height="7.636" viewBox="0 0 29.497 7.636">
<path id="direct_out" d="M26.589,8l-.656-.656,2.7-2.693H.91V3.713H28.635l-2.7-2.7.656-.648,3.818,3.818Z" transform="translate(-0.91 -0.364)" fill="#03543f"/>
</svg>

After

Width:  |  Height:  |  Size: 265 B

View File

@@ -0,0 +1,42 @@
import React, {useEffect, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import { actions as ruleActions } from '../store/rule';
import {string} from "prop-types";
import {Avatar, List, Modal} from "antd";
import {Group} from "../store/group/types";
type Props = {
data?: Group[] | string[] | null;
title?: string;
visible: boolean;
onCancel: () => void;
}
const AccessControlModalGroups:React.FC<Props> = ({data, title, visible, onCancel}) => {
const [isModalVisible, setIsModalVisible] = useState(false);
useEffect(() => setIsModalVisible(visible), [visible])
return (
<>
<Modal title={title} visible={isModalVisible} onCancel={() => onCancel()} footer={null}>
<List
itemLayout="horizontal"
dataSource={data as Group[] | undefined}
renderItem={(item:Group) => (
<List.Item>
<List.Item.Meta
avatar={<Avatar>{item.Name.slice(0,1).toUpperCase()}</Avatar>}
title={item.Name}
description={`${item.PeersCount} peers`}
/>
</List.Item>
)}
/>
</Modal>
</>
);
}
export default AccessControlModalGroups;

View File

@@ -0,0 +1,293 @@
import React, {useEffect, useRef, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import { actions as ruleActions } from '../store/rule';
import { actions as groupsActions } from '../store/group';
import inbound from '../assets/direct_in.svg';
import outbound from '../assets/direct_out.svg';
import {
Col,
Row,
Typography,
Input,
Space,
Radio,
Button, Drawer, Form, List, Divider, Select, Tag
} from "antd";
import {ArrowRightOutlined, CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
import type { CustomTagProps } from 'rc-select/lib/BaseSelect'
import {Rule, RuleToSave} from "../store/rule/types";
import {useAuth0} from "@auth0/auth0-react";
import { uniq } from "lodash"
import {Header} from "antd/es/layout/layout";
const { Paragraph } = Typography;
const { Option } = Select;
interface FormRule extends Rule {
tagSourceGroups: string[]
tagDestinationGroups: string[]
}
const AccessControlNew = () => {
const { getAccessTokenSilently } = useAuth0()
const dispatch = useDispatch()
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible)
const groups = useSelector((state: RootState) => state.group.data)
const rule = useSelector((state: RootState) => state.rule.rule)
const savedRule = useSelector((state: RootState) => state.rule.savedRule)
const [editName, setEditName] = useState(false)
const [tagGroups, setTagGroups] = useState([] as string[])
const [formRule, setFormRule] = useState({} as FormRule)
const [form] = Form.useForm()
const inputNameRef = useRef<any>(null)
useEffect(() => {
if (editName) inputNameRef.current!.focus({
cursor: 'end',
});
}, [editName]);
useEffect(() => {
if (!rule) return
const fRule = {
...rule,
tagSourceGroups: rule.Source ? rule.Source?.map(t => t.Name) : [],
tagDestinationGroups: rule.Destination ? rule.Destination?.map(t => t.Name) : []
} as FormRule
setFormRule(fRule)
form.setFieldsValue(fRule)
}, [rule])
useEffect(() => {
setTagGroups(groups?.map(g => g.Name) || [])
}, [groups])
const createRuleToSave = ():RuleToSave => {
const Source = groups?.filter(g => formRule.tagSourceGroups.includes(g.Name)).map(g => g.ID || '') || []
const Destination = groups?.filter(g => formRule.tagDestinationGroups.includes(g.Name)).map(g => g.ID || '') || []
const sourcesNoId = formRule.tagSourceGroups.filter(s => !tagGroups.includes(s))
const destinationsNoId = formRule.tagDestinationGroups.filter(s => !tagGroups.includes(s))
const groupsToSave = uniq([...sourcesNoId, ...destinationsNoId])
return {
ID: formRule.ID,
Name: formRule.Name,
Source,
Destination,
sourcesNoId,
destinationsNoId,
groupsToSave,
Flow: formRule.Flow
} as RuleToSave
}
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
const ruleToSave = createRuleToSave()
dispatch(ruleActions.saveRule.request({getAccessTokenSilently, payload: ruleToSave}))
})
.catch((errorInfo) => {
console.log('errorInfo', errorInfo)
});
};
const setVisibleNewRule = (status:boolean) => {
dispatch(ruleActions.setSetupNewRuleVisible(status));
}
const onCancel = () => {
if (savedRule.loading) return
setEditName(false)
dispatch(ruleActions.setRule({
Name: '',
Source: [],
Destination: [],
Flow: 'bidirect'
} as Rule))
setVisibleNewRule(false)
}
const onChange = (data:any) => {
setFormRule({...formRule, ...data})
}
const handleChangeSource = (value: string[]) => {
setFormRule({
...formRule,
tagSourceGroups: value
})
};
const handleChangeDestination = (value: string[]) => {
setFormRule({
...formRule,
tagDestinationGroups: value
})
};
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.PeersCount || 0} ${(g.PeersCount && parseInt(g.PeersCount) > 1) ? 'peers' : 'peer'} `
return (
<>
<Tag
color="blue"
style={{ marginRight: 3 }}
>
<strong>{label}</strong>
</Tag>
<span style={{fontSize: ".85em"}}>{peersCount}</span>
</>
)
}
const toggleEditName = (status:boolean) => {
setEditName(status);
}
// const testDeleteGroup = () => {
// groups.forEach(g => {
// dispatch(groupsActions.deleteGroup.request({getAccessTokenSilently, payload: g.ID || ''}))
// })
// }
return (
<>
{rule &&
<Drawer
//title={`${formRule.ID ? 'Edit Rule' : 'New Rule'}`}
headerStyle={{display: "none"}}
forceRender={true}
// width={512}
visible={setupNewRuleVisible}
bodyStyle={{paddingBottom: 80}}
onClose={onCancel}
footer={
<Space style={{display: 'flex', justifyContent: 'end'}}>
<Button onClick={onCancel} disabled={savedRule.loading}>Cancel</Button>
<Button type="primary" disabled={savedRule.loading} onClick={handleFormSubmit}>{`${formRule.ID ? 'Save' : 'Create'}`}</Button>
</Space>
}
>
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
<Row gutter={16}>
<Col span={24}>
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
<Row align="top">
<Col flex="none" style={{display: "flex"}}>
{!editName && formRule.ID &&
<button type="button" aria-label="Close" className="ant-drawer-close"
style={{paddingTop: 3}}
onClick={onCancel}>
<span role="img" aria-label="close" className="anticon anticon-close">
<CloseOutlined size={16}/>
</span>
</button>
}
</Col>
<Col flex="auto">
{ !editName && formRule.ID ? (
<div className={"access-control ant-drawer-title"} onClick={() => toggleEditName(true)}>{formRule.ID ? formRule.Name : 'New Rule'}</div>
) : (
<Form.Item
name="Name"
label={null}
rules={[{required: true, message: 'Please add a name for this access rule'}]}
>
<Input placeholder="Add rule name..." ref={inputNameRef} onPressEnter={() => toggleEditName(false)} onBlur={() => toggleEditName(false)} autoComplete="off"/>
</Form.Item>
)}
</Col>
</Row>
</Header>
</Col>
<Col span={24}>
</Col>
<Col span={24}>
<Form.Item
name="tagSourceGroups"
label={<>Source groups&nbsp;<ArrowRightOutlined /></>}
rules={[{required: true, message: 'Please enter ate least one group'}]}
style={{display: 'flex'}}
>
<Select mode="tags" style={{ width: '100%' }} placeholder="Tags Mode" tagRender={tagRender} onChange={handleChangeSource}>
{
tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option>
)
}
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="tagDestinationGroups"
label={<><ArrowRightOutlined />&nbsp;Destination groups</>}
rules={[{required: true, message: 'Please enter ate least one group'}]}
style={{display: 'flex'}}
>
<Select mode="tags" style={{ width: '100%' }} placeholder="Tags Mode" tagRender={tagRender} onChange={handleChangeDestination}>
{
tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option>
)
}
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Row wrap={false} gutter={12}>
<Col flex="none">
<FlagFilled/>
</Col>
<Col flex="auto">
<Paragraph>
At the moment access rules are bi-directional by default, this means both source and destination can talk to each-other in both directions. However destination peers will not be able to communicate with each other, nor will the source peers.
</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.
</Paragraph>
<a style={{color: 'rgb(07, 114, 128)'}} href="https://docs.netbird.io/overview/access-control" target="_blank">Learn more about access control...</a>
</Col>
</Row>
</Col>
<Col span={24}>
<Divider></Divider>
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
href="https://docs.netbird.io/docs/overview/acls" style={{color: 'rgb(07, 114, 128)'}}>Learn
more about setup keys</Button>
</Col>
</Row>
</Form>
</Drawer>
}
</>
)
}
export default AccessControlNew

View File

@@ -1,52 +0,0 @@
import { XIcon } from "@heroicons/react/outline";
import { useState } from "react";
export default function Banner() {
const [show, setShow] = useState(true);
const dismiss = () => {
setShow(false);
};
return show ? (
<div className="relative bg-indigo-500">
<div className="max-w-7xl mx-auto py-1 px-1 sm:px-6 lg:px-8">
<div className="pr-16 sm:text-center text-white sm:px-16">
<p className="font-normal">
<span className="md:hidden">
Big news! Wiretrustee becomes <strong>netbird</strong>!
</span>
<span className="hidden md:inline">
Big news! Wiretrustee becomes <strong>netbird</strong>!
</span>
<span className="block sm:ml-2 sm:inline-block">
<a
href="https://netbird.io/blog/wiretrustee-becomes-netbird"
className="font-bold underline"
target="_blank"
rel="noreferrer"
>
{" "}
Learn more{" "}
<span aria-hidden="true">&rarr;</span>
</a>
</span>
</p>
</div>
<div className="absolute inset-y-0 right-0 pt-1 pr-1 flex items-start sm:pt-1 sm:pr-2 sm:items-start">
<button
type="button"
className="flex p-1 rounded-md hover:bg-indigo-500 text-white focus:outline-none focus:ring-2 focus:ring-white"
onClick={dismiss}
>
<span className="sr-only">Dismiss</span>
<XIcon
className="h-4 w-4"
aria-hidden="true"
/>
</button>
</div>
</div>
</div>
) : null;
}

54
src/components/Banner.tsx Normal file
View File

@@ -0,0 +1,54 @@
import { useState } from "react";
import {Button, Col, Row, Space, Typography} from "antd";
import { CloseOutlined } from '@ant-design/icons';
const { Text } = Typography
const Banner = () => {
const [show, setShow] = useState(true);
const dismiss = () => {
setShow(false);
};
const linkLearnMore = () => {
return (
<a
href="https://blog.netbird.io/wiretrustee-becomes-netbird"
className="font-bold underline"
target="_blank"
rel="noreferrer"
><Text strong style={{color: "#ffffff"}}>Learn more&nbsp;<span aria-hidden="true">&rarr;</span></Text></a>
)
}
return show ? (
<div className="relative bg-indigo-600 white" color="white" style={{position: "relative", padding: "0.3rem"}} >
<Row>
<Col xs={24} sm={0} lg={0}>
<Text className="ant-col-md-0" style={{color: "#ffffff"}}>
Big news! Wiretrustee becomes <strong>NetBird</strong>!
</Text>
</Col>
<Col xs={24} sm={0} lg={0}>
{linkLearnMore()}
</Col>
</Row>
<Row>
<Col xs={0} sm={24}>
<Space align="center" style={{display: "flex", justifyContent: "center"}}>
<Text style={{color: "#ffffff"}}>
Big news! Wiretrustee becomes <strong>NetBird</strong>!
</Text>
<span>
{linkLearnMore()}
</span>
</Space>
</Col>
</Row>
<Button icon={<CloseOutlined />} onClick={dismiss} size="small" style={{position: "absolute", right: 5, top: 5}}/>
</div>
) : null;
}
export default Banner

View File

@@ -0,0 +1,25 @@
import {copyToClipboard} from "../utils/common";
import {Button, message} from "antd";
import {StepCommand} from "./addpeer/types";
import React from "react";
type Props = {
keyMessage: string;
text: string;
messageText: string;
styleNotification?: any;
style?: any;
className?:any;
};
const ButtonCopyMessage:React.FC<Props> = ({ keyMessage, text, messageText, styleNotification, style, className}) => {
const copyTextMessage = () => {
copyToClipboard(text)
message.success({ content: `${messageText}`, key: keyMessage, duration: 1, style: (styleNotification || {}) });
}
return (
<Button type="text" onClick={copyTextMessage} style={style || {}} className={className}>{text}</Button>
)
}
export default ButtonCopyMessage

View File

@@ -0,0 +1,39 @@
import styled from 'styled-components'
export const Container = styled.div`
max-width: 100%;
margin: 0 auto;
//padding: 0 1rem;
padding: 0;
@media (max-width: 384px) {
max-width: 100%;
padding: 0 16px;
}
@media (min-width: 384px) {
max-width: 100%;
padding: 0 16px;
}
@media (min-width: 576px) {
max-width: 576px;
}
@media (min-width: 768px) {
max-width: 768px;
}
@media (min-width: 992px) {
max-width: 992px;
}
@media (min-width: 1216px) {
max-width: 1216px;
}
&.container-main {
padding-top: 40px !important;
padding-bottom: 40px !important;
}
`

View File

@@ -1,28 +0,0 @@
import React from 'react';
import ImageOne from '../images/egg.jpg';
import ImageTwo from '../images/egg-2.jpg';
const Content = () => {
return (
<>
<div className='menu-card'>
<img src={ImageOne} alt='egg' className='h-full rounded mb-20 shadow' />
<div className='center-content'>
<h2 className='text-2xl mb-2'>Egg Muffins</h2>
<p className='mb-2'>Cripsy, delicious, and nutritious</p>
<span>$16</span>
</div>
</div>
<div className='menu-card'>
<img src={ImageTwo} alt='egg' className='h-full rounded mb-20 shadow' />
<div className='center-content'>
<h2 className='text-2xl mb-2'>Egg Salad</h2>
<p className='mb-2'>Cripsy, delicious, and nutritious</p>
<span>$18</span>
</div>
</div>
</>
);
};
export default Content;

View File

@@ -1,39 +0,0 @@
import React from 'react';
const CopyButton = ({idPrefix, toCopy}) => {
const copyIconId = idPrefix + "copy"
const copySuccessIconId = idPrefix + "copy-success"
const classHidden = "hidden"
const handleKeyCopy = () => {
navigator.clipboard.writeText(toCopy)
let copyIcon = document.getElementById(copyIconId);
let copySuccessIcon = document.getElementById(copySuccessIconId);
copyIcon.classList.add(classHidden);
copySuccessIcon.classList.remove(classHidden);
setTimeout(function() {
copySuccessIcon.classList.add(classHidden);
copyIcon.classList.remove(classHidden);
}, 1200);
}
return (
<button
onClick={handleKeyCopy}
className="whitespace-nowrap text-gray-500 hover:text-gray-400">
<svg id={copyIconId} xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"/>
</svg>
<svg id={copySuccessIconId} xmlns="http://www.w3.org/2000/svg" className="hidden h-6 w-6 text-green-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7"/>
</svg>
</button>
)
}
export default CopyButton;

View File

@@ -1,36 +0,0 @@
import React from 'react';
const CopyButton = ({idPrefix, text}) => {
const copyIconId = idPrefix + "copy"
const copySuccessIconId = idPrefix + "copy-success"
const classHidden = "hidden"
const handleKeyCopy = () => {
navigator.clipboard.writeText(text)
let copyIcon = document.getElementById(copyIconId);
let copySuccessIcon = document.getElementById(copySuccessIconId);
copyIcon.classList.add(classHidden);
copySuccessIcon.classList.remove(classHidden);
setTimeout(function() {
copySuccessIcon.classList.add(classHidden);
copyIcon.classList.remove(classHidden);
}, 1200);
}
return (
<button
onClick={handleKeyCopy}
className="whitespace-nowrap text-gray-500 hover:text-gray-400">
<div id={copyIconId}>
{text}
</div>
<div id={copySuccessIconId} className="flex flex-row hidden text-green-500">
Copied!
</div>
</button>
)
}
export default CopyButton;

View File

@@ -1,103 +0,0 @@
import {Fragment, useEffect, useRef, useState} from 'react'
import {Dialog, Transition} from '@headlessui/react'
import {ExclamationIcon} from '@heroicons/react/outline'
import PropTypes from "prop-types";
const DeleteDialog = ({show, text, title, confirmCallback}) => {
const [open, setOpen] = useState(show)
const cancelButtonRef = useRef(null)
useEffect(() => {
setOpen(show)
}, [show]);
return (
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="fixed z-10 inset-0 overflow-y-auto" initialFocus={cancelButtonRef}
onClose={() =>confirmCallback(false)}>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"/>
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div
className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div
className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true"/>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-m leading-6 text-gray-900">
{title}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
{text}
</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
confirmCallback(true)
}}
>
Confirm
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
confirmCallback(false)
}}
ref={cancelButtonRef}
>
Cancel
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
)
}
DeleteDialog.propTypes = {
show: PropTypes.bool,
confirmCallback: PropTypes.func,
text: PropTypes.string
};
DeleteDialog.defaultProps = {};
export default DeleteDialog;

View File

@@ -1,56 +0,0 @@
import React, {Fragment} from 'react';
import {Menu, Transition} from "@headlessui/react";
import {Link} from "react-router-dom";
import {classNames} from "../utils/common";
const EditButton = ({items, handler}) => {
const handleAction = (action) => {
handler(action)
}
return (
<Menu as="div">
<div>
<Menu.Button
className="whitespace-nowrap text-gray-500 hover:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z"/>
</svg>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="transform opacity-100 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute right-6 bottom-0 mt-2 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
{items.map((item, idx) => (
<Menu.Item key={item.name}>
{({active}) => (
<Link
to="#"
className={classNames(active ? 'bg-gray-100' : 'font-normal', 'block px-4 py-2 text-sm text-gray-700')}
onClick={() => handleAction(item.name)}
>
{item.name}
</Link>
)}
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
)
}
export default EditButton;

View File

@@ -1,29 +0,0 @@
import {Link} from 'react-router-dom'
export default function EmptyPeersPanel() {
return (
<Link
as="button"
to="/add-peer"
className="relative block w-full border-2 border-gray-300 border-dashed rounded p-12 text-center hover:border-gray-400 focus:outline-none"
>
<svg
className="mx-auto h-12 w-12 text-indigo-500"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
strokeLinecap="square"
strokeLinejoin="square"
strokeWidth={2}
d="M8 14v20c0 4.418 7.163 8 16 8 1.381 0 2.721-.087 4-.252M8 14c0 4.418 7.163 8 16 8s16-3.582 16-8M8 14c0-4.418 7.163-8 16-8s16 3.582 16 8m0 0v14m0-4c0 4.418-7.163 8-16 8S8 28.418 8 24m32 10v6m0 0v6m0-6h6m-6 0h-6"
/>
</svg>
<span
className="mt-2 block font-normal text-sm text-gray-700">Let's get started by adding your first peer</span>
</Link>
)
}

View File

@@ -1,13 +0,0 @@
import React from 'react';
const Footer = () => {
return (
<div className='flex justify-center items-center h-24 bg-gray-100 text-gray'>
<p>
Copyright © 2022 <a className="underline text-blue-600 hover:text-blue-800 visited:text-purple-600" href="https://wiretrustee.com">Wiretrustee UG & Authors</a>
</p>
</div>
);
};
export default Footer;

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { Layout } from 'antd';
import {Link} from 'react-router-dom';
const { Footer } = Layout
export default () => {
return (
<Footer style={{ textAlign: 'center' }}>
Copyright © 2022 <a href="https://netbird.io">NetBird Authors</a>
</Footer>
);
};

View File

@@ -1,34 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
const Hero = () => {
return (
<div className='bg-white h-screen flex flex-col justify-center items-center'>
<h1 className='lg:text-9xl md:text-7xl sm:text-5xl text-3xl font-black mb-14'>
EGGCELLENT
</h1>
<Link
className='py-6 px-10 bg-yellow-500 rounded-full text-3xl hover:bg-yellow-300 transition duration-300 ease-in-out flex items-center animate-bounce'
to='/menu'
>
Order Now{' '}
<svg
className='w-6 h-6 ml-4'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z'
/>
</svg>
</Link>
</div>
);
};
export default Hero;

View File

@@ -1,54 +0,0 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import hljs from "highlight.js";
import "highlight.js/styles/mono-blue.css";
//import "highlight.js/styles/base16/flat.css";
import "highlight.js/lib/languages/bash";
class Highlight extends Component {
constructor(props) {
super(props);
this.state = { loaded: false };
this.codeNode = React.createRef();
}
componentDidMount() {
this.setState({ loaded: true });
}
componentDidUpdate() {
this.highlight();
}
highlight = () => {
this.codeNode &&
this.codeNode.current &&
hljs.highlightElement(this.codeNode.current);
};
render() {
const { language, children } = this.props;
const { loaded } = this.state;
if (!loaded) {
return null;
}
return (
<pre className="rounded">
<code ref={this.codeNode} className={language}>
{children}
</code>
</pre>
);
}
}
Highlight.propTypes = {
children: PropTypes.node.isRequired,
language: PropTypes.string,
};
export default Highlight;

View File

@@ -1,13 +0,0 @@
import React from "react";
import loading from "../assets/bars.svg";
const Loading = () => (
<div>
<div className="flex h-screen items-center justify-center" >
<img src={loading} alt="Loading" width="50" height="50"/>
</div>
</div>
);
export default Loading;

View File

@@ -0,0 +1,17 @@
import React from "react";
import loading from "../assets/bars.svg";
import {Space} from "antd";
type Props = {
padding?: string;
width?: string;
height?: string;
};
const Loading:React.FC<Props> = ({padding, width, height}) => (
<Space direction="vertical" align="center" style={{display: 'flex', padding: `${padding || `.25em`}`}}>
<img src={loading} alt="Loading" style={{width: `${width || '25px'}`, height: `${height || '25px'}`}}/>
</Space>
);
export default Loading;

View File

@@ -1,258 +0,0 @@
import React, {Fragment} from 'react';
import {Link, NavLink} from 'react-router-dom';
import logo from "../assets/logo.png";
import defaultProfilePic from "../assets/default-profile.png";
import {Disclosure, Menu, Transition} from '@headlessui/react'
import {MenuIcon, XIcon} from '@heroicons/react/outline'
import {useAuth0} from "@auth0/auth0-react";
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
const Navbar = ({toggle}) => {
const {
user,
isAuthenticated,
logout,
} = useAuth0();
const logoutWithRedirect = () =>
logout({
returnTo: window.location.origin,
});
return (
<Disclosure as="nav" className="bg-white border-b shadow">
{({open}) => (
<>
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
<div className="relative flex justify-between h-16">
<div className="flex">
<div className="flex-shrink-0 flex items-center">
<Link to="/">
<img
className="block lg:hidden h-10 w-auto"
src={logo}
alt="Workflow"
/>
<img
className="hidden lg:block h-10 w-auto"
src={logo}
alt="Workflow"
/>
</Link>
</div>
<div className="hidden md:ml-6 md:flex md:space-x-8">
{isAuthenticated && (
<NavLink
to="/peers"
activeClassName="border-indigo-500 text-gray-900 border-b-2"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
Peers
</NavLink>
)}
{isAuthenticated && (
<NavLink
to="/add-peer"
activeClassName="border-indigo-500 text-gray-900 border-b-2"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
Add Peer
</NavLink>
)}
{isAuthenticated && (
<NavLink
to="/setup-keys"
activeClassName="border-indigo-500 text-gray-900 border-b-2"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
Setup Keys
</NavLink>
)}
{isAuthenticated && (
<NavLink
to="/acls"
activeClassName="border-indigo-500 text-gray-900 border-b-2"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
Access Control
</NavLink>
)}
{isAuthenticated && (
<NavLink
to="/activity"
activeClassName="border-indigo-500 text-gray-900 border-b-2"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
Activity
</NavLink>
)}
{isAuthenticated && (
<NavLink
to="/users"
activeClassName="border-indigo-500 text-gray-900 border-b-2"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
Users
</NavLink>
)}
</div>
</div>
<div className="hidden sm:ml-6 sm:flex sm:items-center">
<Menu as="div" className="ml-3 relative">
<div>
<Menu.Button
className="bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span className="sr-only">Open user menu</span>
<img
className="h-12 w-auto rounded-full"
src={user.picture}
alt=""
onError={({ currentTarget }) => {
currentTarget.onerror = null; // prevents looping
currentTarget.src=defaultProfilePic;
}}
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Item>
<div className="block px-4 py-2 text-sm font-semibold text-gray-700">
{user.email}
</div>
</Menu.Item>
<Menu.Item>
{({active}) => (
<NavLink
to="#"
id="qsLogoutBtn"
className={classNames(active ? 'bg-gray-100 text-gray-900' : 'font-normal', 'block px-4 py-2 text-sm text-gray-700')}
onClick={() => logoutWithRedirect()}
>
Sign out
</NavLink>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
</div>
<div className="-mr-2 flex items-center sm:hidden">
{/* Mobile menu button */}
<Disclosure.Button
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500">
<span className="sr-only">Open main menu</span>
{open ? (
<XIcon className="block h-6 w-6" aria-hidden="true"/>
) : (
<MenuIcon className="block h-6 w-6" aria-hidden="true"/>
)}
</Disclosure.Button>
</div>
</div>
</div>
<Disclosure.Panel className="sm:hidden">
<div className="pt-2 pb-3 space-y-1">
{isAuthenticated && (
<Link
to="/peers"
className="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
>
Peers
</Link>
)}
{isAuthenticated && (
<Link
to="/add-peer"
className="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
>
Add Peer
</Link>
)}
{isAuthenticated && (
<Link
to="/setup-keys"
className="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
>
Setup Keys
</Link>
)}
{isAuthenticated && (
<Link
to="/acls"
className="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
>
Access Control
</Link>
)}
{isAuthenticated && (
<Link
to="/activity"
className="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
>
Activity
</Link>
)}
{isAuthenticated && (
<Link
to="/users"
className="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
>
Users
</Link>
)}
</div>
<div className="pt-4 pb-3 border-t border-gray-200">
<div className="flex items-center px-4">
<div className="flex-shrink-0">
<img
className="h-10 w-10 rounded-full"
src={user.picture}
alt=""
onError={({ currentTarget }) => {
currentTarget.onerror = null; // prevents looping
currentTarget.src=defaultProfilePic;
}}
/>
</div>
<div className="ml-3">
<div className="text-base text-gray-800">{user.email}</div>
</div>
</div>
<div className="mt-3 space-y-1">
<Link
to="#"
className="block px-4 py-2 text-base text-gray-500 hover:text-gray-800 hover:bg-gray-100"
onClick={() => logoutWithRedirect()}
>
Sign out
</Link>
</div>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};
export default Navbar;

118
src/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,118 @@
import React, {useEffect, useState} from 'react';
import {Link} from 'react-router-dom';
import logo from "../assets/logo.png";
import {useAuth0} from "@auth0/auth0-react";
import {useLocation} from 'react-router-dom';
import {Menu, Row, Col, Grid, Dropdown, Avatar, Button, Typography, Space} from 'antd'
import {ItemType} from "antd/lib/menu/hooks/useItems";
import {UserOutlined} from "@ant-design/icons";
import {AvatarSize} from "antd/es/avatar/SizeContext";
const { Text } = Typography
const { useBreakpoint } = Grid;
const Navbar = () => {
let location = useLocation();
const {
user,
isAuthenticated,
logout,
} = useAuth0();
const screens = useBreakpoint();
const [hideMenuUser, setHideMenuUser] = useState(false)
const userEmailKey = 'user-email'
const userLogoutKey = 'user-logout'
const userDividerKey = 'user-divider'
const [menuItems, setMenuItems] = useState([
{ label: (<Link to="/peers">Peers</Link>), key: '/peers' },
{ label: (<Link to="/add-peer">Add Peer</Link>), key: '/add-peer' },
{ label: (<Link to="/setup-keys">Setup Keys</Link>), key: '/setup-keys' },
/*{ label: (<Link to="/acls">Access Control</Link>), key: '/acls' },
{ label: (<Link to="/activity">Activity</Link>), key: '/activity' },*/
{ label: (<Link to="/users">Users</Link>), key: '/users' }
] as ItemType[])
useEffect(() => {
const fs = menuItems.filter(m => m?.key !== userEmailKey && m?.key !== userLogoutKey && m?.key !== userDividerKey)
if (screens.xs === true) {
setHideMenuUser(false)
fs.push({ type: 'divider', key: userDividerKey })
fs.push({
label: (
<Link to="#">{user?.name}</Link>
),
icon: createAvatar("small"),
key: userEmailKey
})
fs.push({ label: (<Button type="link" block onClick={logoutWithRedirect}>Logout</Button>), key: userLogoutKey })
setMenuItems([...fs])
return
}
setMenuItems([...fs])
setHideMenuUser(true)
}, [screens])
const menuUser = (
<Menu
items={[
{
label: <>{user?.email}</>,
key: '0',
},
{
label: <a onClick={e => {
logoutWithRedirect()
e.preventDefault()}
}>Logout</a>,
key: '1',
}
]}
/>
);
const logoutWithRedirect = () =>
logout({
returnTo: window.location.origin,
});
const createAvatar = (size:AvatarSize) => {
return user?.picture ? (
<Avatar size={size} src={user?.picture}/>
) : (
<Avatar size={size}>{(user?.name || '').slice(0, 1).toUpperCase()}</Avatar>
)
}
return (
<>
<Row justify="space-evenly" align="middle">
<Col flex="0 1 60px">
<Link id="logo" to="/">
<img
alt="logo"
style={{width: "55px"}}
src={logo}
/>
</Link>
</Col>
<Col flex="1 1 auto">
<div>
<Menu mode="horizontal" selectable={true} selectedKeys={[location.pathname]} defaultSelectedKeys={[location.pathname]} items={menuItems}/>
</div>
</Col>
{hideMenuUser &&
<Col>
<Dropdown overlay={menuUser} placement="bottomRight" trigger={['click']}>
{createAvatar("large")}
</Dropdown>
</Col>
}
</Row>
</>
);
};
export default Navbar;

View File

@@ -1,240 +0,0 @@
import {Fragment, useEffect, useState} from 'react'
import {Dialog, RadioGroup, Transition} from '@headlessui/react'
import {XIcon} from '@heroicons/react/outline'
import {ExclamationCircleIcon, QuestionMarkCircleIcon} from '@heroicons/react/solid'
import PropTypes from "prop-types";
const types = [
{name: 'Reusable', description: 'This type of a setup key allows to setup multiple machine', value: 'reusable'},
{name: 'One-off', description: 'This key can be used only once', value: 'one-off'},
]
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
const NewSetupKeyDialog = ({show, closeCallback}) => {
const [open, setOpen] = useState(show)
const [keyName, setKeyName] = useState("")
const [selectedType, setSelectedType] = useState(types[0])
useEffect(() => {
setOpen(show)
setKeyName("")
setSelectedType(types[0])
}, [show]);
return (
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="fixed inset-0 overflow-hidden" onClose={() => {
closeCallback(true, null, null, null)
}}>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"/>
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-lg">
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll">
<div className="flex-1">
{/* Header */}
<div className="px-4 py-6 bg-white sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-700">New setup key</Dialog.Title>
<p className="text-sm text-gray-500">
Setup keys allow you to enroll new peers
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="text-gray-400 hover:text-gray-500"
onClick={() => {
closeCallback(true, null, null, null)
}}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true"/>
</button>
</div>
</div>
</div>
<hr className="border-gray-200"/>
<div
className="py-6 space-y-6 sm:py-0 sm:space-y-0 bg-gray-50">
<div
className="space-y-1 px-4 sm:space-y-0 grid grid-cols-2 sm:gap-4 sm:px-6 sm:py-5">
<div>
<label
htmlFor="project-name"
className="block text-gray-900 sm:mt-px sm:pt-2"
>
Name
</label>
</div>
<div className="mt-1 relative col-span-2">
<input
type="text"
name="new-setup-key-name"
id="new-setup-key-name"
className="w-full shadow-sm text-lg focus:ring-gray-300 rounded focus:border-gray-300 border border-gray-300"
value={keyName}
onChange={event => setKeyName(event.target.value)}
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<ExclamationCircleIcon id="name-validation-error-icon" className="h-5 w-5 text-red-400 hidden" aria-hidden="true" />
</div>
</div>
<div className="sm:col-span-2">
<p id="name-validation-error" className="mt-2 text-sm text-red-600 hidden">
The name of the key can't be empty.
</p>
</div>
</div>
<fieldset>
<div
className="space-y-2 px-4 sm:space-y-0 grid sm:grid-cols-2 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
<div>
<legend
className="text-gray-900">Type
</legend>
</div>
<div className="space-y-5 sm:col-span-2">
<RadioGroup value={selectedType} onChange={setSelectedType}>
<RadioGroup.Label className="sr-only">Privacy
setting</RadioGroup.Label>
<div className="bg-white rounded -space-y-px">
{types.map((setting, settingIdx) => (
<RadioGroup.Option
key={setting.name}
value={setting}
className={({checked}) =>
classNames(
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
settingIdx === types.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
checked ? 'bg-gray-50 border-gray-200 z-10' : 'border-gray-200',
'relative border p-4 flex cursor-pointer focus:outline-none'
)
}
>
{({active, checked}) => (
<>
<span
className={classNames(
checked ? 'bg-gray-600 border-transparent' : 'bg-white border-gray-300',
active ? 'ring-2 ring-offset-2 ring-gray-500' : '',
'h-4 w-4 mt-0.5 cursor-pointer rounded border flex items-center justify-center'
)}
aria-hidden="true"
>
<span
className="rounded bg-white w-1.5 h-1.5"/>
</span>
<div className="ml-3 flex flex-col">
<RadioGroup.Label
as="span"
className={classNames(checked ? 'text-gray-900' : 'text-gray-900', 'block text-sm')}
>
{setting.name}
</RadioGroup.Label>
<RadioGroup.Description
as="span"
className={classNames(checked ? 'text-gray-700' : 'text-gray-500', 'block text-sm')}
>
{setting.description}
</RadioGroup.Description>
</div>
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
<hr className="border-gray-200"/>
<div
className="flex flex-col space-between space-y-4 sm:flex-row sm:items-center sm:space-between sm:space-y-0">
<div>
<a
href="https://docs.netbird.io/overview/setup-keys"
target="_blank"
rel="noreferrer"
className="group flex items-center text-sm text-gray-500 hover:text-gray-900 space-x-2.5"
>
<QuestionMarkCircleIcon
className="h-5 w-5 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
<span>Learn more about setup keys</span>
</a>
</div>
</div>
</div>
</div>
</fieldset>
</div>
</div>
{/* Action buttons */}
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
<div className="space-x-3 flex justify-end">
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
closeCallback(true, null, null, null)
}}
>
Cancel
</button>
<button
type="button"
className="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto"
onClick={() => {
if (!keyName) {
let el = document.getElementById("name-validation-error");
el.classList.remove("hidden")
el = document.getElementById("name-validation-error-icon");
el.classList.remove("hidden")
} else {
closeCallback(false, keyName, selectedType.value, null)
}
}}
>
Create
</button>
</div>
</div>
</form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
NewSetupKeyDialog.propTypes = {
show: PropTypes.bool,
closeCallback: PropTypes.func,
/*text: PropTypes.string*/
};
NewSetupKeyDialog.defaultProps = {};
export default NewSetupKeyDialog;

View File

@@ -1,233 +0,0 @@
import React, {useEffect, useState} from "react";
import {ChevronLeftIcon, ChevronRightIcon} from "@heroicons/react/solid";
import {withAuthenticationRequired} from "@auth0/auth0-react";
import Loading from "../components/Loading";
import PropTypes from "prop-types";
import LinuxTab from "./addpeer/LinuxTab";
// @data the data that will be paginated
// @RenderComponent the component that needs to be rendered
// @pageLimit number of Elements shown in Pagination bar
// @dataLimit maximum Elements rendered per page
const PaginatedPeersList = (props) => {
const [pageCount, setPageCount] = useState(0); // actual pageCount we have
const [currentPage, setCurrentPage] = useState(1);
function recalculate() {
setPageCount(Math.ceil(props.data.length / props.dataLimit))
}
// sliding window of size pageLimit for shown elements of bar
function goToNextPage() {
if (currentPage === pageCount) return;
setCurrentPage((page) => page + 1);
}
function goToPreviousPage() {
if (currentPage === 1) return;
setCurrentPage((page) => page - 1);
}
function changePage(event) {
const pageNumber = Number(event.target.textContent);
setCurrentPage(pageNumber);
}
function goToFirst() {
setCurrentPage(1);
}
function goToLast() {
setCurrentPage(pageCount);
}
const getStartIndex = () => {
return currentPage * props.dataLimit - props.dataLimit;
}
const getEndIndex = () => {
return getStartIndex() + props.dataLimit;
}
const getPaginatedData = () => {
const startIndex = getStartIndex();
const endIndex = getEndIndex();
return props.data.slice(startIndex, endIndex);
};
const compressPagination = () => {
// if the pageLimit is greater than the actual number of pages we have, just render all pages
if (props.pageLimit > pageCount) {
return [...Array(pageCount).keys()].map((index) => index + 1);
}
// if the currentPage is already presented in the paginationBar we can just leave the bar alone
// center the currentPage
let bar = [];
let offset = Math.floor(props.pageLimit / 2);
if (currentPage - offset <= 1) {
return [...Array(props.pageLimit).keys()].map((index) => index + 1);
}
if (currentPage + offset > pageCount) {
for (let i = pageCount - props.pageLimit + 1; i <= pageCount; i++)
bar.push(i);
return bar;
}
for (let i = offset; i > 0; i--) {
bar.push(currentPage - i);
}
bar.push(currentPage);
for (let i = 1; i <= offset; i++) {
bar.push(currentPage + i);
}
return bar;
};
function PaginationBarElem(props) {
let default_btn =
"z-10 bg-white border-gray-300 text-gray-700 relative inline-flex items-center px-4 py-2 border hover:bg-gray-50";
let clicked_btn =
"z-10 bg-gray-50 border-gray-500 text-gray-600 relative inline-flex items-center px-4 py-2 border hover:bg-gray-50";
return (
<button
aria-current="page"
className={
props.pageNo === props.clicked ? clicked_btn : default_btn
}
onClick={changePage}
>
{props.pageNo}
</button>
);
}
useEffect(() => {
recalculate()
}, [props.data]);
return (
<>
<div className="flex flex-col">
<div className="-my-2 sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{[
"Name",
"IP",
"Status",
"Last Seen",
"OS",
"Version",
].map((col) => {
return (
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
key={col}
>
{col}
</th>
);
})}
<th
scope="col"
className="relative px-6 py-3"
>
<span className="sr-only">
Edit
</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{getPaginatedData().map((elem) =>
props.RenderComponent(elem)
)}
</tbody>
</table>
<div
className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className=" text-gray-700">
Showing{" "}
<span className="font-medium">{props.data.length === 0 ? 0 : getStartIndex() + 1}</span>{" "}
to <span className="font-medium">{props.data.length === 0 ? 0 : getStartIndex() + getPaginatedData().length}</span>{" "}
of <span
className="font-medium">{props.data.length}</span> {props.data.length === 1 ? "peer" : "peers"}
</p>
</div>
{pageCount === 1 || pageCount === 0 ? (
<div/>
) : (
<div>
<nav
className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px"
aria-label="Pagination"
>
<button
className="relative inline-flex rounded-l-md items-center px-2 py-2 border border-gray-300 bg-white text-gray-500 hover:bg-gray-50"
onClick={goToFirst}
>
First
</button>
<button
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-gray-500 hover:bg-gray-50"
onClick={goToPreviousPage}
>
<span className="sr-only">Previous</span>
<ChevronLeftIcon
className="h-5 w-5"
aria-hidden="true"
/>
</button>
<div>
{compressPagination().map((elem) => {
return (
<PaginationBarElem
clicked={currentPage}
pageNo={elem}
key={elem}
/>
);
})}
</div>
<button
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-gray-500 hover:bg-gray-50"
onClick={goToNextPage}
>
<span className="sr-only">Next</span>
<ChevronRightIcon
className="h-5 w-5"
aria-hidden="true"
/>
</button>
<button
className="relative inline-flex rounded-r-md items-center px-2 py-2 border border-gray-300 bg-white text-gray-500 hover:bg-gray-50"
onClick={goToLast}
>
Last
</button>
</nav>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
};
export default PaginatedPeersList;

View File

@@ -0,0 +1,141 @@
import React, {useEffect, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import { actions as setupKeyActions } from '../store/setup-key';
import {
Col,
Row,
Typography,
Input,
Space,
Radio,
Button, Drawer, Form, List, Divider
} from "antd";
import {RootState} from "typesafe-actions";
import {QuestionCircleFilled} from "@ant-design/icons";
import {SetupKey} from "../store/setup-key/types";
import {useAuth0} from "@auth0/auth0-react";
const { Text } = Typography;
const SetupKeyNew = () => {
const { getAccessTokenSilently } = useAuth0()
const dispatch = useDispatch()
const setupNewKeyVisible = useSelector((state: RootState) => state.setupKey.setupNewKeyVisible)
const setupKey = useSelector((state: RootState) => state.setupKey.setupKey)
const createdSetupKey = useSelector((state: RootState) => state.setupKey.createdSetupKey)
const [formSetupKey, setFormSetupKey] = useState({} as SetupKey)
const [form] = Form.useForm()
useEffect(() => {
setFormSetupKey({ ...setupKey } as SetupKey)
form.setFieldsValue(setupKey)
}, [setupKey])
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
dispatch(setupKeyActions.createSetupKey.request({getAccessTokenSilently, payload: formSetupKey}))
})
.catch((errorInfo) => {
console.log('errorInfo', errorInfo)
});
};
const setVisibleNewSetupKey = (status:boolean) => {
dispatch(setupKeyActions.setSetupNewKeyVisible(status));
}
const onCancel = () => {
if (createdSetupKey.loading) return
dispatch(setupKeyActions.setSetupKey({
Name: '',
Type: 'reusable'
} as SetupKey))
setVisibleNewSetupKey(false)
}
const onChange = (data:any) => {
setFormSetupKey({...formSetupKey, ...data})
}
return (
<>
{setupKey &&
<Drawer
title="New setup key"
forceRender={true}
// width={512}
visible={setupNewKeyVisible}
bodyStyle={{paddingBottom: 80}}
onClose={onCancel}
footer={
<Space style={{display: 'flex', justifyContent: 'end'}}>
<Button disabled={createdSetupKey.loading} onClick={onCancel}>Cancel</Button>
<Button disabled={createdSetupKey.loading} type="primary" onClick={handleFormSubmit}>Create</Button>
</Space>
}
>
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
<Row gutter={16}>
<Col span={24}>
<Form.Item
name="Name"
label="Name"
rules={[{required: true, message: 'Please enter key name'}]}
>
<Input placeholder="Please enter key name" autoComplete="off"/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="Type"
label="Type"
rules={[{required: true, message: 'Please enter key type'}]}
style={{display: 'flex'}}
>
<Radio.Group style={{display: 'flex'}}>
<Space direction="vertical" style={{flex: 1}}>
<List
size="large"
bordered
>
<List.Item>
<Radio value={"reusable"}>
<Space direction="vertical" size="small">
<Text strong>Reusable</Text>
<Text>This type of a setup key allows to setup multiple
machine</Text>
</Space>
</Radio>
</List.Item>
<List.Item>
<Radio value={"one-off"}>
<Space direction="vertical" size="small">
<Text strong>One-off</Text>
<Text>This key can be used only once</Text>
</Space>
</Radio>
</List.Item>
</List>
</Space>
</Radio.Group>
</Form.Item>
</Col>
<Col span={24}>
<Divider></Divider>
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
href="https://docs.netbird.io/docs/overview/setup-keys" style={{color: 'rgb(07, 114, 128)'}}>Learn
more about setup keys</Button>
</Col>
</Row>
</Form>
</Drawer>
}
</>
)
}
export default SetupKeyNew

View File

@@ -1,74 +0,0 @@
import LinuxTab from "./LinuxTab";
import {useEffect, useState} from "react";
import {classNames} from "../../utils/common";
import WindowsTab from "./WindowsTab";
import MacTab from "./MacTab";
const tabs = [
{name: 'Linux', idx: 1},
{name: 'Windows', idx: 2},
{name: 'MacOS', idx: 3}
]
const AddPeerTabSelector = ({setupKey}) => {
const [openTab, setOpenTab] = useState(1);
const detectOS = () => {
let os = 1;
if (navigator.userAgent.indexOf("Win")!==-1) os=2;
if (navigator.userAgent.indexOf("Mac")!==-1) os=3;
if (navigator.userAgent.indexOf("X11")!==-1) os=1;
if (navigator.userAgent.indexOf("Linux")!==-1) os=1
setOpenTab(os)
}
useEffect(() => {
detectOS();
}, []);
return (
<div>
<div>
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<button
key={tab.name}
onClick={e => {
e.preventDefault();
setOpenTab(tab.idx)
}}
className={classNames(
tab.idx === openTab
? 'border-indigo-500 text-gray-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
'whitespace-nowrap py-4 px-1 border-b-2 text-m'
)}
aria-current={tab.idx === openTab ? 'page' : undefined}
>
{tab.name}
</button>
))}
</nav>
</div>
</div>
<div className="w-full mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-8 sm:px-0">
<div className={openTab === 1 ? "block" : "hidden"} id="linux-installation-steps">
<LinuxTab setupKey={setupKey}/>
</div>
<div className={openTab === 2 ? "block" : "hidden"} id="windows-installation-steps">
<WindowsTab setupKey={setupKey}/>
</div>
<div className={openTab === 3 ? "block" : "hidden"} id="macos-installation-steps">
<MacTab setupKey={setupKey}/>
</div>
</div>
</div>
</div>
)
}
export default AddPeerTabSelector;

View File

@@ -1,110 +0,0 @@
import ArrowCircleRightIcon from "@heroicons/react/outline/ArrowCircleRightIcon";
import Highlight from "../Highlight";
import CopyButton from "../CopyButton";
import {classNames} from "../../utils/common";
import PropTypes from "prop-types";
import {getConfig} from "../../config";
const {grpcApiOrigin} = getConfig();
const LinuxTab = ({setupKey}) => {
const steps = [
{
id: 1,
target: 'Add repository:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
commands: ["sudo apt install ca-certificates curl gnupg -y", "curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo apt-key add -", "echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list"],
copy: true
},
{
id: 2,
target: 'Install Netbird:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["sudo apt-get update", "# for CLI only\nsudo apt-get install netbird", "# for GUI package\nsudo apt-get install netbird-ui"]
},
{
id: 3,
target: 'Run Netbird and log in the browser:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: grpcApiOrigin === '' ? ["sudo netbird up"] : ["sudo netbird up --management-url " + grpcApiOrigin]
},
{
id: 4,
target: 'Get your IP address:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["ip addr show wt0"]
},
]
const formatCommands = (commands, key) => {
return commands.map(c => key != null ? c.replace("<PASTE-SETUP-KEY>", key.Key) : c).join("\n")
}
return (
<ol className="overflow-hidden">
{steps.map((step, stepIdx) => (
<li key={"linux-tab-step-" + step.id}
className={classNames(stepIdx !== steps.length - 1 ? 'pb-10' : '', 'relative')}>
<>
{stepIdx !== steps.length - 1 ? (
<div
className="-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full bg-gray-300"
aria-hidden="true"/>
) : null}
<a href={step.href} className="relative flex items-start group">
<span className="h-9 " aria-hidden="true">
<span
className="relative z-10 w-8 h-8 flex items-center justify-center bg-white border-2 border-gray-300 rounded group-hover:border-gray-400">
<span className="text-m text-gray-700">{step.id}</span>
</span>
</span>
<span className="ml-4 min-w-0 ">
<span className="tracking-wide text-gray-700">{step.target}</span>
<div className="flex flex-col space-y-2 ">
<span
className="text-sm text-gray-500">
{
step.content != null ? (step.content) : (
step.commands && (<Highlight language="bash">
{formatCommands(step.commands, setupKey)}
</Highlight>)
)
}
</span>
{step.copy && (<CopyButton toCopy={formatCommands(step.commands, setupKey)}
idPrefix={"add-peer-code-" + step.id}/>)}
</div>
</span>
</a>
</>
</li>
))}
</ol>
)
}
export default LinuxTab;
LinuxTab.propTypes = {
setupKey: PropTypes.object,
};
LinuxTab.defaultProps = {};

View File

@@ -0,0 +1,81 @@
import React, {useState} from 'react';
import { Button } from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
import {getConfig} from "../../config";
const {grpcApiOrigin} = getConfig();
export const LinuxTab = () => {
const formatNetBirdUP = () => {
let cmd = "sudo netbird up"
if (grpcApiOrigin) {
cmd = "sudo netbird up --management-url " + grpcApiOrigin
}
return [
cmd
].join('\n')
}
const [steps, setSteps] = useState([
{
key: 1,
title: 'Add repository:',
commands: [
`sudo apt install ca-certificates curl gnupg -y`,
`curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo apt-key add -`,
`echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 2,
title: 'Install NetBird:',
commands: [
`sudo apt-get update`,
`# for CLI only`,
`sudo apt-get install netbird`,
`# for GUI package`,
`sudo apt-get install netbird-ui`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 3,
title: 'Run NetBird and log in the browser:',
commands: formatNetBirdUP(),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 4,
title: 'Get your IP address:',
commands: [
`ip addr show wt0`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand
])
/*const clickTest = () => {
steps.push({
key: steps.length+1,
title: `Test ${steps.length+1}`,
commands: [`hi lorena!`].join('\n'),
copied: false
})
console.log(steps)
setSteps([...steps])
}*/
return (
<TabSteps stepsItems={steps}/>
)
}
export default LinuxTab

View File

@@ -1,118 +0,0 @@
import ArrowCircleRightIcon from "@heroicons/react/outline/ArrowCircleRightIcon";
import Highlight from "../Highlight";
import CopyButton from "../CopyButton";
import {classNames} from "../../utils/common";
import PropTypes from "prop-types";
import {getConfig} from "../../config";
const {grpcApiOrigin} = getConfig();
const MacTab = ({setupKey}) => {
const steps = [
{
id: 1,
target: 'Download and install Brew (package manager):',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: <button type="button"
onClick={() => window.open("https://brew.sh/")}
className="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto">
Download Brew
</button>,
commands: [],
copy: false
},
{
id: 2,
target: 'Install Netbird:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["# for CLI only\nbrew install netbirdio/tap/netbird", "# for GUI package\nbrew install --cask netbirdio/tap/netbird-ui"]
},
{
id: 3,
target: 'Run Netbird and log in the browser:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: grpcApiOrigin === '' ? ["sudo netbird up"] : ["sudo netbird up --management-url " + grpcApiOrigin]
},
{
id: 4,
target: 'Get your IP address:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["sudo ifconfig utun100"]
},
]
const formatCommands = (commands, key) => {
return commands.map(c => key != null ? c.replace("<PASTE-SETUP-KEY>", key.Key) : c).join("\n")
}
return (
<ol className="overflow-hidden">
{steps.map((step, stepIdx) => (
<li key={"linux-tab-step-" + step.id}
className={classNames(stepIdx !== steps.length - 1 ? 'pb-10' : '', 'relative')}>
<>
{stepIdx !== steps.length - 1 ? (
<div
className="-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full bg-gray-300"
aria-hidden="true"/>
) : null}
<a href={step.href} className="relative flex items-start group">
<span className="h-9 " aria-hidden="true">
<span
className="relative z-10 w-8 h-8 flex items-center justify-center bg-white border-2 border-gray-300 rounded group-hover:border-gray-400">
<span className="text-m text-gray-700">{step.id}</span>
</span>
</span>
<span className="ml-4 min-w-0 ">
<span className="tracking-wide text-gray-700">{step.target}</span>
<div className="flex flex-col space-y-2 ">
<span
className="text-sm text-gray-500">
{
step.content != null ? (
<div className="font-mono underline mt-4">
{step.content}
</div>
) : (
step.commands && (<Highlight language="bash">
{formatCommands(step.commands, setupKey)}
</Highlight>)
)
}
</span>
{step.copy && (<CopyButton toCopy={formatCommands(step.commands, setupKey)}
idPrefix={"add-peer-code-mac-" + step.id}/>)}
</div>
</span>
</a>
</>
</li>
))}
</ol>
)
}
export default MacTab;
MacTab.propTypes = {
setupKey: PropTypes.object,
};
MacTab.defaultProps = {};

View File

@@ -0,0 +1,55 @@
import React, {useState} from 'react';
import { Button } from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
export const LinuxTab = () => {
const [steps, setSteps] = useState([
{
key: 1,
title: 'Download and install Brew (package manager)',
commands: (
<Button type="primary" href="https://brew.sh/" target="_blank">Download Brew</Button>
),
copied: false
} as StepCommand,
{
key: 2,
title: 'Install NetBird:',
commands: [
`# for CLI only`,
`brew install netbirdio/tap/netbird`,
`# for GUI package`,
`brew install --cask netbirdio/tap/netbird-ui`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 3,
title: 'Run NetBird and log in the browser:',
commands: [
`sudo netbird up`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 4,
title: 'Get your IP address:',
commands: [
`sudo ifconfig utun100`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand
])
return (
<TabSteps stepsItems={steps}/>
)
}
export default LinuxTab

View File

@@ -1,103 +0,0 @@
import {Fragment, useState} from 'react'
import {Listbox, Transition} from '@headlessui/react'
import {CheckIcon, SelectorIcon} from '@heroicons/react/solid'
import CopyButton from "../CopyButton";
import {classNames} from "../../utils/common";
import PropTypes from "prop-types";
const SetupKeySelect = ({data, onSelected}) => {
const [selected, setSelected] = useState(data.length > 0 ? data[0] : {Name: "...", Id: "none"})
const handleSelected = selectedKey => {
setSelected(selectedKey)
onSelected(selectedKey)
let keyBox = document.getElementById("key-box");
keyBox.classList.remove("hidden")
};
return (
<div>
<span className="text-m tracking-wide text-gray-700">Select setup key to register peer:</span>
<span className="ml-4 min-w-0">
<div className="flex flex-col space-y-2">
<Listbox value={selected} onChange={handleSelected}>
{({open}) => (
<>
<div className="mt-1 relative">
<Listbox.Button
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span className="block truncate">{selected.Name}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
{data.map((item) => (
<Listbox.Option
key={item.Id}
className={({active}) =>
classNames(
active ? 'text-white bg-indigo-600' : 'text-gray-900',
'cursor-default select-none relative py-2 pl-3 pr-9'
)
}
value={item}
>
{({selected, active}) => (
<>
<span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>
{item.Name}
</span>
{selected ? (
<span
className={classNames(
active ? 'text-white' : 'text-indigo-600',
'absolute inset-y-0 right-0 flex items-center pr-4'
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
<div id="key-box" className="hidden rounded-md bg-gray-100 p-4">
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-gray-700">{selected.Key}</p>
<p className="mt-4 text-sm md:mt-0 md:ml-6">
<CopyButton toCopy={selected.Key}/>
</p>
</div>
</div>
</div>
</span>
</div>
)
}
SetupKeySelect.propTypes = {
data: PropTypes.array,
onSelected: PropTypes.func,
};
SetupKeySelect.defaultProps = {};
export default SetupKeySelect

View File

@@ -0,0 +1,79 @@
import {useDispatch, useSelector} from "react-redux";
import Highlight from 'react-highlight';
import "highlight.js/styles/mono-blue.css";
import "highlight.js/lib/languages/bash";
import { StepCommand } from './types'
import {
Typography,
Space,
Steps, Button
} from "antd";
import {copyToClipboard} from "../../utils/common";
import {CheckOutlined, CopyOutlined} from "@ant-design/icons";
import React, {useEffect, useState} from "react";
const { Title, Text } = Typography;
const { Step } = Steps;
type Props = {
stepsItems: Array<StepCommand>
};
const TabSteps:React.FC<Props> = ({stepsItems}) => {
const [steps, setSteps] = useState(stepsItems)
useEffect(() => setSteps(stepsItems), [stepsItems])
const onCopyClick = (key: string | number, commands:React.ReactNode | string, copied: boolean) => {
if (!(typeof commands === 'string')) return
copyToClipboard(commands)
const step = steps.find(s => s.key === key)
if (step) step.copied = copied
setSteps([...steps])
if (copied) {
setTimeout(() => {
onCopyClick(key, commands, false)
}, 2000)
}
}
return (
<Steps direction="vertical" current={0}>
{steps.map(c =>
<Step
key={c.key}
title={c.title}
description={
<Space className="nb-code" direction="vertical" size="small" style={{display: "flex"}}>
{ (c.commands && (typeof c.commands === 'string' || c.commands instanceof String)) ? (
<Highlight className='bash'>
{c.commands}
</Highlight>
) : (
c.commands
)}
{ c.showCopyButton &&
<>
{ !c.copied ? (
<Button type="text" size="large" className="btn-copy-code" icon={<CopyOutlined/>}
style={{color: "rgb(107, 114, 128)"}}
onClick={() => onCopyClick(c.key, c.commands, true)}/>
): (
<Button type="text" size="large" className="btn-copy-code" icon={<CheckOutlined/>}
style={{color: "green"}}/>
)}
</>
}
</Space>
}
/>
)}
</Steps>
)
}
export default TabSteps;

View File

@@ -1,110 +0,0 @@
import ArrowCircleRightIcon from "@heroicons/react/outline/ArrowCircleRightIcon";
import Highlight from "../Highlight";
import CopyButton from "../CopyButton";
import {classNames} from "../../utils/common";
import PropTypes from "prop-types";
import {getConfig} from "../../config";
const {grpcApiOrigin} = getConfig();
const WindowsTab = ({setupKey}) => {
const steps = [
{
id: 1,
target: 'Download and run Windows installer:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: <button type="button"
onClick={() => window.open("https://github.com/netbirdio/netbird/releases/latest/download/netbird_installer_0.6.1_windows_amd64.exe")}
className="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto">
Download Netbird
</button>,
commands: [],
copy: false
},
{
id: 2,
target: 'Click on "Connect" from the Netbird icon in your system tray.',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: "",
copy: false,
commands: []
},
{
id: 3,
target: 'Log in your browser.',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: "",
copy: false,
commands: []
},
]
const formatCommands = (commands, key) => {
return commands.map(c => key != null ? c.replace("<PASTE-SETUP-KEY>", key.Key) : c).join("\n")
}
return (
<ol className="overflow-hidden">
{steps.map((step, stepIdx) => (
<li key={"linux-tab-step-" + step.id}
className={classNames(stepIdx !== steps.length - 1 ? 'pb-10' : '', 'relative')}>
<>
{stepIdx !== steps.length - 1 ? (
<div
className="-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full bg-gray-300"
aria-hidden="true"/>
) : null}
<a href={step.href} className="relative flex items-start group">
<span className="h-9 " aria-hidden="true">
<span
className="relative z-10 w-8 h-8 flex items-center justify-center bg-white border-2 border-gray-300 rounded group-hover:border-gray-400">
<span className="text-m text-gray-700">{step.id}</span>
</span>
</span>
<span className="ml-4 min-w-0 ">
<span className="tracking-wide text-gray-700">{step.target}</span>
<div className="flex flex-col space-y-2 ">
<span
className="text-sm text-gray-500">
{
step.content != null ? (
<div className="font-mono underline mt-4">
{step.content}
</div>
) : (
step.commands && (<Highlight language="bash">
{formatCommands(step.commands, setupKey)}
</Highlight>)
)
}
</span>
{step.copy && (<CopyButton toCopy={formatCommands(step.commands, setupKey)}
idPrefix={"add-peer-code-win-" + step.id}/>)}
</div>
</span>
</a>
</>
</li>
))}
</ol>
)
}
export default WindowsTab;
WindowsTab.propTypes = {
setupKey: PropTypes.object,
};
WindowsTab.defaultProps = {};

View File

@@ -0,0 +1,40 @@
import React, {useState} from 'react';
import { Button } from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
export const WindowsTab = () => {
const releaseVersion = '0.6.2'
const [steps, setSteps] = useState([
{
key: 1,
title: 'Download and run Windows installer:',
commands: (
<Button type="primary" href={`https://github.com/netbirdio/netbird/releases/download/v${releaseVersion}/netbird_installer_${releaseVersion}_windows_amd64.exe`} target="_blank">Download NetBird</Button>
),
copied: false
} as StepCommand,
{
key: 2,
title: 'Click on "Connect" from the NetBird icon in your system tray.',
commands: '',
copied: false,
showCopyButton: false
},
{
key: 3,
title: 'Log in your browser.\n',
commands: '',
copied: false,
showCopyButton: false
}
])
return (
<TabSteps stepsItems={steps}/>
)
}
export default WindowsTab

View File

@@ -0,0 +1,9 @@
import * as React from "react";
export interface StepCommand {
key: number | string,
title: string,
commands: React.ReactNode | string | null,
copied?: boolean,
showCopyButton?: boolean
}

View File

@@ -1,4 +1,4 @@
let configJson = "";
let configJson:any = "";
if (process.env.NODE_ENV !== 'production') {
configJson = require("./.local-config.json");

View File

@@ -1,6 +1,41 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import '~antd/dist/antd.css';
/*@tailwind base;*/
/*@tailwind components;*/
/*@tailwind utilities;*/
body {
font-size: 16px;
background-color: #f0f2f5;
}
.ant-layout-header {
background: #fff;
padding: 0;
height: auto;
min-height: 64px;
border-bottom: 1px solid #e0e0e0;
}
.ant-layout-content {
background-color: rgb(249, 250, 251);
min-height: 600px;
}
.ant-menu-horizontal {
border: none;
}
.ant-menu-horizontal > .ant-menu-item a {
color: rgba(107, 114, 128, 1);
font-weight: 500;
}
.ant-menu-horizontal > .ant-menu-item-selected a {
color: rgba(17, 24, 39, 1);
}
.menu-card {
@apply flex flex-col justify-center items-center bg-white h-screen py-40;
@@ -9,3 +44,85 @@
.center-content {
@apply flex flex-col justify-center items-center;
}
.space-align-container {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
}
.space-align-block {
flex: none;
margin: 8px 4px;
padding: 4px;
border: none;
}
.space-align-block .mock-block {
display: inline-block;
padding: 32px 8px 16px;
background: rgba(150, 150, 150, 0.2);
}
.bg-indigo-600{
background-color: rgb(79, 70, 229);
}
.card-table-no-placeholder .ant-table-placeholder{
display: none;
}
.card-table .ant-pagination {
padding: 0 16px;
}
.card-table .ant-empty {
height: 200px;
}
.card-table .ant-table-ping-left:not(.ant-table-has-fix-left) .ant-table-container::before {
box-shadow: none;
}
.card-table .ant-table-ping-right:not(.ant-table-has-fix-right) .ant-table-container::after {
box-shadow: none;
}
.select-rows-per-page-en .ant-select-selector::before {
content: 'Rows per page';
width: 104px;
align-self: center;
}
.ant-steps-item-tail::after {
background-color: #1890ff !important;
}
.ant-steps-item-icon {
background: #1890ff !important;
color: #ffffff !important;
border: none;
}
.ant-steps-icon {
color: #ffffff !important;
}
.nb-code {
margin-bottom: 1em;
}
.nb-code pre {
margin-bottom: 0;
}
.nb-code code {
border-radius: .25em;
}
.ant-steps-item-title {
color: rgba(0, 0, 0, .85) !important;
}
.access-control.ant-drawer-title:hover {
text-decoration: underline;
cursor: pointer;
}

View File

@@ -1,20 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {BrowserRouter} from 'react-router-dom';
import history from "./utils/history";
import { getConfig } from "./config";
import {Auth0Provider} from "@auth0/auth0-react";
import {BrowserRouter} from "react-router-dom";
const onRedirectCallback = (appState) => {
const onRedirectCallback = (appState:any) => {
history.push(
appState && appState.returnTo ? appState.returnTo : window.location.pathname
);
};
const config = getConfig();
const providerConfig = {
@@ -26,6 +25,7 @@ const providerConfig = {
onRedirectCallback,
};
/*
ReactDOM.render(
<Auth0Provider {...providerConfig}>
<React.StrictMode>
@@ -36,6 +36,19 @@ ReactDOM.render(
</Auth0Provider>,
document.getElementById('root')
);
*/
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<BrowserRouter>
<Auth0Provider {...providerConfig}>
<App/>
</Auth0Provider>
</BrowserRouter>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))

View File

@@ -0,0 +1,68 @@
import { Method } from 'axios';
import { ApiClientParams, ApiResponse, RequestConfig } from './types';
import { apiRequest } from './api-request';
class ApiClient {
urlBase: string;
constructor(params: ApiClientParams) {
this.urlBase = params.urlBase;
}
request<T>(
method: Method,
url: string,
data?: unknown,
config?: RequestConfig,
): Promise<ApiResponse<T>> {
return apiRequest<T>({
url,
data,
method,
urlBase: this.urlBase,
...config,
});
}
get<T>(
url: string,
data?: unknown,
config?: RequestConfig,
): Promise<ApiResponse<T>> {
return this.request<T>('GET', url, data, config);
}
post<T>(
url: string,
data?: unknown,
config?: RequestConfig,
): Promise<ApiResponse<T>> {
return this.request<T>('POST', url, data, config);
}
put<T>(
url: string,
data?: unknown,
config?: RequestConfig,
): Promise<ApiResponse<T>> {
return this.request<T>('PUT', url, data, config);
}
patch<T>(
url: string,
data?: unknown,
config?: RequestConfig,
): Promise<ApiResponse<T>> {
return this.request<T>('PATCH', url, data, config);
}
delete<T>(
url: string,
data?: unknown,
config?: RequestConfig,
): Promise<ApiResponse<T>> {
return this.request<T>('DELETE', url, data, config);
}
}
export { ApiClient };

View File

@@ -0,0 +1,45 @@
import axios, {AxiosError} from 'axios';
import {ApiRequestParams, ApiResponse, ApiError} from './types';
import {headersFactory, RequestHeader} from './header-factory';
/*axios.interceptors.response.use(undefined, err => {
let res = err.response;
if (res.status === 401) {
}
})*/
async function apiRequest<T>(params: ApiRequestParams): Promise<ApiResponse<T>> {
const data = params.data ? (params.data as any).payload : undefined;
const url = `${params.urlBase}${params.url}`;
const extraHeaders = params.extraHeaders || {};
const headers = await headersFactory((params.data as any).getAccessTokenSilently);
const builtHeader: RequestHeader = { ...headers, ...extraHeaders };
let response;
let error:ApiError = {
code: '-1',
message: '',
data: null,
statusCode: -1
};
try {
response = await axios.request({ url, data, method: params.method, headers: builtHeader as any });
} catch (err: any) {
error = <ApiError>{
code: err ? err.code : '-1',
message: err ? err.message : '',
data: (err && err.response) ? err.response.data : null,
statusCode: (err && err.response) ? err.response.status : -1,
};
throw error;
}
return { statusCode: response ? response.status : error.statusCode, body: response ? response.data : error };
}
export { apiRequest };

View File

@@ -0,0 +1,23 @@
export interface RequestHeader {
'Content-Type': string;
[key: string]: unknown;
}
const headersFactory = async (getAccessTokenSilently:any): Promise<RequestHeader> => {
const headers: RequestHeader = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
//const token = await getLocalItem<string>(StorageKey.token);
//const token = ''
const token = await getAccessTokenSilently()
if (token) {
headers.authorization = `Bearer ${token}`;
}
return headers;
};
export { headersFactory };

View File

@@ -0,0 +1,10 @@
import { ApiClient } from './api-client';
import {getConfig} from "../../config";
const {apiOrigin} = getConfig();
const apiClient = new ApiClient({
urlBase: `${apiOrigin}`
});
export { apiClient };

View File

@@ -0,0 +1,63 @@
import { Method } from 'axios';
export interface RequestPayload<T> {
getAccessTokenSilently: any;
payload:T;
}
export interface RequestHeader {
'content-type': string;
[key: string]: string;
}
export interface ApiResponse<T> {
statusCode: number;
body: T;
}
export interface ApiClientParams {
urlBase: string;
}
export interface RequestConfig {
onUploadProgress?: (event: ProgressEvent) => void;
extraHeaders?: Record<string, unknown>;
}
export interface ApiRequestParams extends RequestConfig {
method: Method;
url: string;
data: unknown;
urlBase: string;
}
export interface ApiError {
code:string;
message:string;
data?:any;
statusCode:number;
}
export interface DeleteResponse<T> {
loading: boolean;
success: boolean;
failure: boolean;
error: ApiError | null;
data : T;
}
export interface CreateResponse<T> {
loading: boolean;
success: boolean;
failure: boolean;
error: ApiError | null;
data : T;
}
export interface ChangeResponse<T> {
loading: boolean;
success: boolean;
failure: boolean;
error: ApiError | null;
data : T;
}

View File

@@ -0,0 +1,29 @@
export enum StorageKey {
token
}
const setLocalItem = async <T>(key: StorageKey, value: T): Promise<void> => {
try {
localStorage.setItem(`@net-bird:${key}`, JSON.stringify(value));
} catch (err) {
console.log(err);
}
};
const getLocalItem = async <T>(key: StorageKey): Promise<T | null> => {
try {
const item = localStorage.getItem(`@net-bird:${key}`);
if (!item) {
return null;
}
return JSON.parse(item) as T;
} catch (err) {
console.log(err);
}
return null;
};
export {
getLocalItem,
setLocalItem
}

View File

@@ -0,0 +1,30 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import { Group } from './types';
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
const actions = {
getGroups: createAsyncAction(
'GET_GROUPS_REQUEST',
'GET_GROUPS_SUCCESS',
'GET_GROUPS_FAILURE',
)<RequestPayload<null>, Group[], ApiError>(),
saveGroup: createAsyncAction(
'CREATE_GROUP_REQUEST',
'CREATE_GROUP_SUCCESS',
'CREATE_GROUP_FAILURE',
)<RequestPayload<Group>, CreateResponse<Group | null>, CreateResponse<Group | null>>(),
setCreateGroup: createAction('SET_CREATE_GROUP')<CreateResponse<Group | null>>(),
deleteGroup: createAsyncAction(
'DELETE_GROUP_REQUEST',
'DELETE_GROUP_SUCCESS',
'DELETE_GROUP_FAILURE'
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeleteGroup: createAction('SET_DELETE_GROUP')<DeleteResponse<string | null>>(),
removeGroup: createAction('REMOVE_GROUP')<string>(),
setGroup: createAction('SET_GROUP')<Group>(),
};
export type ActionTypes = ActionType<typeof actions>;
export default actions;

7
src/store/group/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import actions, { ActionTypes as _actionTypes } from './actions';
import reducer from './reducer';
import sagas from './sagas';
export type ActionTypes = _actionTypes;
export { actions, reducer, sagas };

View File

@@ -0,0 +1,89 @@
import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { Group } from './types';
import actions, { ActionTypes } from './actions';
import {ApiError, DeleteResponse, CreateResponse, ChangeResponse} from "../../services/api-client/types";
type StateType = Readonly<{
data: Group[] | null;
group: Group | null;
loading: boolean;
failed: ApiError | null;
saving: boolean;
deletedGroup: DeleteResponse<string | null>;
revokedGroup: ChangeResponse<Group | null>;
savedGroup: CreateResponse<Group | null>;
}>;
const initialState: StateType = {
data: [],
group: null,
loading: false,
failed: null,
saving: false,
deletedGroup: <DeleteResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
revokedGroup: <ChangeResponse<Group | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
savedGroup: <CreateResponse<Group | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
}
};
const data = createReducer<Group[], ActionTypes>(initialState.data as Group[])
.handleAction(actions.getGroups.success,(_, action) => action.payload)
.handleAction(actions.getGroups.failure, () => []);
const group = createReducer<Group, ActionTypes>(initialState.group as Group)
.handleAction(actions.setGroup, (store, action) => action.payload);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getGroups.request, () => true)
.handleAction(actions.getGroups.success, () => false)
.handleAction(actions.getGroups.failure, () => false);
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
.handleAction(actions.getGroups.request, () => null)
.handleAction(actions.getGroups.success, () => null)
.handleAction(actions.getGroups.failure, (store, action) => action.payload);
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
.handleAction(actions.getGroups.request, () => true)
.handleAction(actions.getGroups.success, () => false)
.handleAction(actions.getGroups.failure, () => false);
const deletedGroup = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletedGroup)
.handleAction(actions.deleteGroup.request, () => initialState.deletedGroup)
.handleAction(actions.deleteGroup.success, (store, action) => action.payload)
.handleAction(actions.deleteGroup.failure, (store, action) => action.payload)
.handleAction(actions.setDeleteGroup, (store, action) => action.payload);
const savedGroup = createReducer<CreateResponse<Group | null>, ActionTypes>(initialState.savedGroup)
.handleAction(actions.saveGroup.request, () => initialState.savedGroup)
.handleAction(actions.saveGroup.success, (store, action) => action.payload)
.handleAction(actions.saveGroup.failure, (store, action) => action.payload)
.handleAction(actions.setCreateGroup, (store, action) => action.payload)
export default combineReducers({
data,
group,
loading,
failed,
saving,
deletedGroup,
savedGroup,
});

116
src/store/group/sagas.ts Normal file
View File

@@ -0,0 +1,116 @@
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
import { Group } from './types'
import service from './service';
import actions from './actions';
export function* getGroups(action: ReturnType<typeof actions.getGroups.request>): Generator {
try {
yield put(actions.setDeleteGroup({
loading: false,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>))
const effect = yield call(service.getGroups, action.payload);
const response = effect as ApiResponse<Group[]>;
yield put(actions.getGroups.success(response.body));
} catch (err) {
yield put(actions.getGroups.failure(err as ApiError));
}
}
export function* setCreateGroup(action: ReturnType<typeof actions.setCreateGroup>): Generator {
yield put(actions.setCreateGroup(action.payload))
}
export function* saveGroup(action: ReturnType<typeof actions.saveGroup.request>): Generator {
try {
yield put(actions.setCreateGroup({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as CreateResponse<Group | null>))
let effect
if (action.payload.payload.ID) {
effect = yield call(service.editGroup, action.payload);
} else {
effect = yield call(service.createGroup, action.payload);
}
const response = effect as ApiResponse<Group>;
yield put(actions.saveGroup.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as CreateResponse<Group | null>));
const setupKeys = [...(yield select(state => state.setupKey.data)) as Group[]]
setupKeys.unshift(response.body)
yield put(actions.getGroups.success(setupKeys));
} catch (err) {
yield put(actions.saveGroup.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as CreateResponse<Group | null>));
}
}
export function* setDeleteGroup(action: ReturnType<typeof actions.setDeleteGroup>): Generator {
yield put(actions.setDeleteGroup(action.payload))
}
export function* deleteGroup(action: ReturnType<typeof actions.deleteGroup.request>): Generator {
try {
yield call(actions.setDeleteGroup,{
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
const effect = yield call(service.deleteGroup, action.payload);
const response = effect as ApiResponse<any>;
yield put(actions.deleteGroup.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
const rules = (yield select(state => state.rule.data)) as Group[]
yield put(actions.getGroups.success(rules.filter((p:Group) => p.ID !== action.payload.payload)))
} catch (err) {
yield put(actions.deleteGroup.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as DeleteResponse<string | null>));
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getGroups.request, getGroups),
takeLatest(actions.saveGroup.request, saveGroup),
takeLatest(actions.deleteGroup.request, deleteGroup)
]);
}

View File

@@ -0,0 +1,37 @@
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import {Group} from './types';
const baseUrl = `/api/groups`
export default {
async getGroups(payload:RequestPayload<null>): Promise<ApiResponse<Group[]>> {
return apiClient.get<Group[]>(
`${baseUrl}`,
payload
);
},
async getGroup(payload:RequestPayload<null>): Promise<ApiResponse<Group>> {
return apiClient.get<Group>(
`${baseUrl}/` + payload.payload,
payload
);
},
async deleteGroup(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`${baseUrl}/` + payload.payload,
payload
);
},
async createGroup(payload:RequestPayload<Group>): Promise<ApiResponse<Group>> {
return apiClient.post<Group>(
`${baseUrl}`,
payload
);
},
async editGroup(payload:RequestPayload<Group>): Promise<ApiResponse<Group>> {
return apiClient.put<Group>(
`${baseUrl}`,
payload
);
},
};

6
src/store/group/types.ts Normal file
View File

@@ -0,0 +1,6 @@
export interface Group {
ID?: string;
Name: string;
Peers?: any[];
PeersCount?: string;
}

27
src/store/index.ts Normal file
View File

@@ -0,0 +1,27 @@
import { legacy_createStore as createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { composeWithDevTools } from 'redux-devtools-extension';
import { sagas as peerSagas } from './peer';
import { sagas as setupKeySagas } from './setup-key';
import { sagas as userSagas } from './user';
import { sagas as ruleSagas } from './rule';
import { sagas as groupSagas } from './group';
import rootReducer from './root-reducer';
import { apiClient } from '../services/api-client';
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
const enhancer = composeWithDevTools(applyMiddleware(...middlewares));
const store = createStore(rootReducer, enhancer);
sagaMiddleware.run(peerSagas);
sagaMiddleware.run(setupKeySagas);
sagaMiddleware.run(userSagas);
sagaMiddleware.run(ruleSagas);
sagaMiddleware.run(groupSagas);
export { apiClient, rootReducer, store };

22
src/store/peer/actions.ts Normal file
View File

@@ -0,0 +1,22 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import { Peer } from './types';
import {ApiError, DeleteResponse, RequestPayload} from '../../services/api-client/types';
const actions = {
getPeers: createAsyncAction(
'GET_PEERS_REQUEST',
'GET_PEERS_SUCCESS',
'GET_PEERS_FAILURE',
)<RequestPayload<null>, Peer[], ApiError>(),
deletedPeer: createAsyncAction(
'DELETE_PEER_REQUEST',
'DELETE_PEER_SUCCESS',
'DELETE_PEER_FAILURE'
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeletePeer: createAction('SET_DELETE_PEER')<DeleteResponse<string | null>>(),
removePeer: createAction('REMOVE_PEER')<string>(),
setPeer: createAction('SET_PEER')<Peer>(),
};
export type ActionTypes = ActionType<typeof actions>;
export default actions;

7
src/store/peer/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import actions, { ActionTypes as _actionTypes } from './actions';
import reducer from './reducer';
import sagas from './sagas';
export type ActionTypes = _actionTypes;
export { actions, reducer, sagas };

66
src/store/peer/reducer.ts Normal file
View File

@@ -0,0 +1,66 @@
import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { Peer } from './types';
import actions, { ActionTypes } from './actions';
import {ApiError, DeleteResponse} from "../../services/api-client/types";
type StateType = Readonly<{
data: Peer[] | null;
peer: Peer | null;
loading: boolean;
failed: ApiError | null;
saving: boolean;
deletedPeer: DeleteResponse<string | null>;
}>;
const initialState: StateType = {
data: [],
peer: null,
loading: false,
failed: null,
saving: false,
deletedPeer: <DeleteResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
}
};
const data = createReducer<Peer[], ActionTypes>(initialState.data as Peer[])
.handleAction(actions.getPeers.success,(_, action) => action.payload)
.handleAction(actions.getPeers.failure, () => []);
const peer = createReducer<Peer, ActionTypes>(initialState.peer as Peer)
.handleAction(actions.setPeer, (store, action) => action.payload);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getPeers.request, () => true)
.handleAction(actions.getPeers.success, () => false)
.handleAction(actions.getPeers.failure, () => false);
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
.handleAction(actions.getPeers.request, () => null)
.handleAction(actions.getPeers.success, () => null)
.handleAction(actions.getPeers.failure, (store, action) => action.payload);
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
.handleAction(actions.getPeers.request, () => true)
.handleAction(actions.getPeers.success, () => false)
.handleAction(actions.getPeers.failure, () => false);
const deletedPeer = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletedPeer)
.handleAction(actions.deletedPeer.request, () => initialState.deletedPeer)
.handleAction(actions.deletedPeer.success, (store, action) => action.payload)
.handleAction(actions.deletedPeer.failure, (store, action) => action.payload)
.handleAction(actions.setDeletePeer, (store, action) => action.payload)
export default combineReducers({
data,
peer,
loading,
failed,
saving,
deletedPeer
});

71
src/store/peer/sagas.ts Normal file
View File

@@ -0,0 +1,71 @@
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
import { Peer } from './types'
import service from './service';
import actions from './actions';
export function* getPeers(action: ReturnType<typeof actions.getPeers.request>): Generator {
try {
yield put(actions.setDeletePeer({
loading: false,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>))
const effect = yield call(service.getPeers, action.payload);
const response = effect as ApiResponse<Peer[]>;
yield put(actions.getPeers.success(response.body));
} catch (err) {
yield put(actions.getPeers.failure(err as ApiError));
}
}
export function* setDeletePeer(action: ReturnType<typeof actions.setDeletePeer>): Generator {
yield put(actions.setDeletePeer(action.payload))
}
export function* deletePeer(action: ReturnType<typeof actions.deletedPeer.request>): Generator {
try {
yield call(actions.setDeletePeer,{
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
const effect = yield call(service.deletedPeer, action.payload);
const response = effect as ApiResponse<any>;
yield put(actions.deletedPeer.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
const peers = (yield select(state => state.peer.data)) as Peer[]
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.IP !== action.payload.payload)))
} catch (err) {
yield put(actions.deletedPeer.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as DeleteResponse<string | null>));
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getPeers.request, getPeers),
takeLatest(actions.deletedPeer.request, deletePeer)
]);
}

18
src/store/peer/service.ts Normal file
View File

@@ -0,0 +1,18 @@
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import { Peer } from './types';
export default {
async getPeers(payload:RequestPayload<null>): Promise<ApiResponse<Peer[]>> {
return apiClient.get<Peer[]>(
`/api/peers`,
payload
);
},
async deletedPeer(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`/api/peers/` + payload.payload,
payload
);
}
};

9
src/store/peer/types.ts Normal file
View File

@@ -0,0 +1,9 @@
export interface Peer {
Name: string,
IP: string,
Connected: boolean,
LastSeen: string,
OS: string,
Version: string,
Groups?: any[]
}

13
src/store/root-action.ts Normal file
View File

@@ -0,0 +1,13 @@
import { actions as PeerActions } from './peer';
import { actions as SetupKeyActions } from './setup-key';
import { actions as UserActions } from './user';
import { actions as GroupActions } from './group';
import { actions as RuleActions } from './rule';
export default {
peer: PeerActions,
setupKey: SetupKeyActions,
user: UserActions,
group: GroupActions,
rule: RuleActions
};

15
src/store/root-reducer.ts Normal file
View File

@@ -0,0 +1,15 @@
import { combineReducers } from 'redux';
import { reducer as peer } from './peer';
import { reducer as setupKey } from './setup-key';
import { reducer as user } from './user';
import { reducer as group } from './group';
import { reducer as rule } from './rule';
export default combineReducers({
peer,
setupKey,
user,
group,
rule
});

31
src/store/rule/actions.ts Normal file
View File

@@ -0,0 +1,31 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import {Rule, RuleToSave} from './types';
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
const actions = {
getRules: createAsyncAction(
'GET_RULES_REQUEST',
'GET_RULES_SUCCESS',
'GET_RULES_FAILURE',
)<RequestPayload<null>, Rule[], ApiError>(),
saveRule: createAsyncAction(
'SAVE_RULE_REQUEST',
'SAVE_RULE_SUCCESS',
'SAVE_RULE_FAILURE',
)<RequestPayload<RuleToSave>, CreateResponse<Rule | null>, CreateResponse<Rule | null>>(),
setSavedRule: createAction('SET_CREATE_RULE')<CreateResponse<Rule | null>>(),
deleteRule: createAsyncAction(
'DELETE_RULE_REQUEST',
'DELETE_RULE_SUCCESS',
'DELETE_RULE_FAILURE'
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeletedRule: createAction('SET_DELETED_RULE')<DeleteResponse<string | null>>(),
removeRule: createAction('REMOVE_RULE')<string>(),
setRule: createAction('SET_RULE')<Rule>(),
setSetupNewRuleVisible: createAction('SET_SETUP_NEW_RULE_VISIBLE')<boolean>()
};
export type ActionTypes = ActionType<typeof actions>;
export default actions;

7
src/store/rule/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import actions, { ActionTypes as _actionTypes } from './actions';
import reducer from './reducer';
import sagas from './sagas';
export type ActionTypes = _actionTypes;
export { actions, reducer, sagas };

87
src/store/rule/reducer.ts Normal file
View File

@@ -0,0 +1,87 @@
import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { Rule } from './types';
import actions, { ActionTypes } from './actions';
import {ApiError, DeleteResponse, CreateResponse, ChangeResponse} from "../../services/api-client/types";
type StateType = Readonly<{
data: Rule[] | null;
rule: Rule | null;
loading: boolean;
failed: ApiError | null;
saving: boolean;
deleteRule: DeleteResponse<string | null>;
savedRule: CreateResponse<Rule | null>;
setupNewRuleVisible: boolean
}>;
const initialState: StateType = {
data: [],
rule: null,
loading: false,
failed: null,
saving: false,
deleteRule: <DeleteResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
savedRule: <CreateResponse<Rule | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
setupNewRuleVisible: false
};
const data = createReducer<Rule[], ActionTypes>(initialState.data as Rule[])
.handleAction(actions.getRules.success,(_, action) => action.payload)
.handleAction(actions.getRules.failure, () => []);
const rule = createReducer<Rule, ActionTypes>(initialState.rule as Rule)
.handleAction(actions.setRule, (store, action) => action.payload);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getRules.request, () => true)
.handleAction(actions.getRules.success, () => false)
.handleAction(actions.getRules.failure, () => false);
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
.handleAction(actions.getRules.request, () => null)
.handleAction(actions.getRules.success, () => null)
.handleAction(actions.getRules.failure, (store, action) => action.payload);
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
.handleAction(actions.getRules.request, () => true)
.handleAction(actions.getRules.success, () => false)
.handleAction(actions.getRules.failure, () => false);
const deletedRule = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deleteRule)
.handleAction(actions.deleteRule.request, () => initialState.deleteRule)
.handleAction(actions.deleteRule.success, (store, action) => action.payload)
.handleAction(actions.deleteRule.failure, (store, action) => action.payload)
.handleAction(actions.setDeletedRule, (store, action) => action.payload);
const savedRule = createReducer<CreateResponse<Rule | null>, ActionTypes>(initialState.savedRule)
.handleAction(actions.saveRule.request, () => initialState.savedRule)
.handleAction(actions.saveRule.success, (store, action) => action.payload)
.handleAction(actions.saveRule.failure, (store, action) => action.payload)
.handleAction(actions.setSavedRule, (store, action) => action.payload)
const setupNewRuleVisible = createReducer<boolean, ActionTypes>(initialState.setupNewRuleVisible)
.handleAction(actions.setSetupNewRuleVisible, (store, action) => action.payload)
export default combineReducers({
data,
rule,
loading,
failed,
saving,
deletedRule,
savedRule,
setupNewRuleVisible
});

155
src/store/rule/sagas.ts Normal file
View File

@@ -0,0 +1,155 @@
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
import {Rule, RuleToSave} from './types'
import service from './service';
import serviceGroup from '../group/service';
import actions from './actions';
import { actions as groupActions } from '../group';
import {Group} from "../group/types";
export function* getRules(action: ReturnType<typeof actions.getRules.request>): Generator {
try {
yield put(actions.setDeletedRule({
loading: false,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>))
const effect = yield call(service.getRules, action.payload);
const response = effect as ApiResponse<Rule[]>;
yield put(actions.getRules.success(response.body));
} catch (err) {
yield put(actions.getRules.failure(err as ApiError));
}
}
export function* setCreatedRule(action: ReturnType<typeof actions.setSavedRule>): Generator {
yield put(actions.setSavedRule(action.payload))
}
function getNewGroupIds(dataString:string[], responses:Group[]):string[] {
return responses.filter(r => dataString.includes(r.Name)).map(r => r.ID || '')
}
export function* saveRule(action: ReturnType<typeof actions.saveRule.request>): Generator {
try {
yield put(actions.setSavedRule({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as CreateResponse<Rule | null>))
const ruleToSave = action.payload.payload
const responsesGroup = yield all(ruleToSave.groupsToSave.map(g => call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: { Name: g }
})
))
const resGroups = (responsesGroup as ApiResponse<Rule>[]).filter(r => r.statusCode === 200).map(r => (r.body as Group))
const currentGroups = [...(yield select(state => state.group.data)) as Rule[]]
const newGroups = [...currentGroups, ...resGroups]
yield put(groupActions.getGroups.success(newGroups));
console.log(resGroups)
console.log(ruleToSave.groupsToSave)
const newSources = getNewGroupIds(ruleToSave.sourcesNoId, resGroups)
const newDestinations = getNewGroupIds(ruleToSave.destinationsNoId, resGroups)
console.log(newDestinations)
const payloadToSave = {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: {
Name: ruleToSave.Name,
Source: [...ruleToSave.Source as string[], ...newSources],
Destination: [...ruleToSave.Destination as string[], ...newDestinations],
Flow: ruleToSave.Flow
} as Rule
}
let effect
if (!ruleToSave.ID) {
effect = yield call(service.createRule, payloadToSave);
} else {
payloadToSave.payload.ID = ruleToSave.ID
effect = yield call(service.editRule, payloadToSave);
}
const response = effect as ApiResponse<Rule>;
yield put(actions.saveRule.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as CreateResponse<Rule | null>));
yield put(groupActions.getGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
yield put(actions.getRules.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
} catch (err) {
yield put(actions.saveRule.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as CreateResponse<Rule | null>));
}
}
export function* setDeleteRule(action: ReturnType<typeof actions.setDeletedRule>): Generator {
yield put(actions.setDeletedRule(action.payload))
}
export function* deleteRule(action: ReturnType<typeof actions.deleteRule.request>): Generator {
try {
yield call(actions.setDeletedRule,{
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
const effect = yield call(service.deletedRule, action.payload);
const response = effect as ApiResponse<any>;
yield put(actions.deleteRule.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
const rules = (yield select(state => state.rule.data)) as Rule[]
yield put(actions.getRules.success(rules.filter((p:Rule) => p.ID !== action.payload.payload)))
} catch (err) {
yield put(actions.deleteRule.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as DeleteResponse<string | null>));
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getRules.request, getRules),
takeLatest(actions.saveRule.request, saveRule),
takeLatest(actions.deleteRule.request, deleteRule)
]);
}

31
src/store/rule/service.ts Normal file
View File

@@ -0,0 +1,31 @@
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import { Rule } from './types';
import {SetupKey} from "../setup-key/types";
export default {
async getRules(payload:RequestPayload<null>): Promise<ApiResponse<Rule[]>> {
return apiClient.get<Rule[]>(
`/api/rules`,
payload
);
},
async deletedRule(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`/api/rules/` + payload.payload,
payload
);
},
async createRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
return apiClient.post<Rule>(
`/api/rules`,
payload
);
},
async editRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
return apiClient.put<Rule>(
`/api/rules`,
payload
);
},
};

15
src/store/rule/types.ts Normal file
View File

@@ -0,0 +1,15 @@
import {Group} from "../group/types";
export interface Rule {
ID?: string
Name: string
Source: Group[] | string[] | null
Destination: Group[] | string[] | null
Flow: string
}
export interface RuleToSave extends Rule {
sourcesNoId: string[],
destinationsNoId: string[],
groupsToSave: string[]
}

View File

@@ -0,0 +1,45 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import {SetupKey, SetupKeyRevoke} from './types';
import {
ApiError,
ChangeResponse,
CreateResponse,
DeleteResponse,
RequestPayload
} from '../../services/api-client/types';
const actions = {
getSetupKeys: createAsyncAction(
'GET_SETUP_KEYS_REQUEST',
'GET_SETUP_KEYS_SUCCESS',
'GET_SETUP_KEYS_FAILURE',
)<RequestPayload<null>, SetupKey[], ApiError>(),
createSetupKey: createAsyncAction(
'CREATE_SETUP_KEY_REQUEST',
'CREATE_SETUP_KEY_SUCCESS',
'CREATE_SETUP_KEY_FAILURE',
)<RequestPayload<SetupKey>, CreateResponse<SetupKey | null>, CreateResponse<SetupKey | null>>(),
setCreateSetupKey: createAction('SET_CREATE_SETUP_KEY')<CreateResponse<SetupKey | null>>(),
deleteSetupKey: createAsyncAction(
'DELETE_SETUP_KEY_REQUEST',
'DELETE_SETUP_KEY_SUCCESS',
'DELETE_SETUP_KEY_FAILURE'
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeleteSetupKey: createAction('SET_DELETE_SETUP_KEY')<DeleteResponse<string | null>>(),
revokeSetupKey: createAsyncAction(
'REVOKE_SETUP_KEY_REQUEST',
'REVOKE_SETUP_KEY_SUCCESS',
'REVOKE_SETUP_KEY_FAILURE'
)<RequestPayload<SetupKeyRevoke>, ChangeResponse<SetupKey | null>, ChangeResponse<SetupKey | null>>(),
setRevokeSetupKey: createAction('SET_REVOKE_SETUP_KEY')<ChangeResponse<SetupKey | null>>(),
removeSetupKey: createAction('REMOVE_SETUP_KEY')<string>(),
setSetupKey: createAction('SET_SETUP_KEY')<SetupKey>(),
setSetupNewKeyVisible: createAction('SET_SETUP_NEW_KEY_VISIBLE')<boolean>()
};
export type ActionTypes = ActionType<typeof actions>;
export default actions;

View File

@@ -0,0 +1,7 @@
import actions, { ActionTypes as _actionTypes } from './actions';
import reducer from './reducer';
import sagas from './sagas';
export type ActionTypes = _actionTypes;
export { actions, reducer, sagas };

View File

@@ -0,0 +1,102 @@
import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { SetupKey } from './types';
import actions, { ActionTypes } from './actions';
import {ApiError, DeleteResponse, CreateResponse, ChangeResponse} from "../../services/api-client/types";
type StateType = Readonly<{
data: SetupKey[] | null;
setupKey: SetupKey | null;
loading: boolean;
failed: ApiError | null;
saving: boolean;
deletedSetupKey: DeleteResponse<string | null>;
revokedSetupKey: ChangeResponse<SetupKey | null>;
createdSetupKey: CreateResponse<SetupKey | null>;
setupNewKeyVisible: boolean
}>;
const initialState: StateType = {
data: [],
setupKey: null,
loading: false,
failed: null,
saving: false,
deletedSetupKey: <DeleteResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
revokedSetupKey: <ChangeResponse<SetupKey | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
createdSetupKey: <CreateResponse<SetupKey | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
setupNewKeyVisible: false
};
const data = createReducer<SetupKey[], ActionTypes>(initialState.data as SetupKey[])
.handleAction(actions.getSetupKeys.success,(_, action) => action.payload)
.handleAction(actions.getSetupKeys.failure, () => []);
const setupKey = createReducer<SetupKey, ActionTypes>(initialState.setupKey as SetupKey)
.handleAction(actions.setSetupKey, (store, action) => action.payload);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getSetupKeys.request, () => true)
.handleAction(actions.getSetupKeys.success, () => false)
.handleAction(actions.getSetupKeys.failure, () => false);
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
.handleAction(actions.getSetupKeys.request, () => null)
.handleAction(actions.getSetupKeys.success, () => null)
.handleAction(actions.getSetupKeys.failure, (store, action) => action.payload);
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
.handleAction(actions.getSetupKeys.request, () => true)
.handleAction(actions.getSetupKeys.success, () => false)
.handleAction(actions.getSetupKeys.failure, () => false);
const deletedSetupKey = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletedSetupKey)
.handleAction(actions.deleteSetupKey.request, () => initialState.deletedSetupKey)
.handleAction(actions.deleteSetupKey.success, (store, action) => action.payload)
.handleAction(actions.deleteSetupKey.failure, (store, action) => action.payload)
.handleAction(actions.setDeleteSetupKey, (store, action) => action.payload);
const revokedSetupKey = createReducer<ChangeResponse<SetupKey | null>, ActionTypes>(initialState.revokedSetupKey)
.handleAction(actions.revokeSetupKey.request, () => initialState.revokedSetupKey)
.handleAction(actions.revokeSetupKey.success, (store, action) => action.payload)
.handleAction(actions.revokeSetupKey.failure, (store, action) => action.payload)
.handleAction(actions.setRevokeSetupKey, (store, action) => action.payload)
const createdSetupKey = createReducer<CreateResponse<SetupKey | null>, ActionTypes>(initialState.createdSetupKey)
.handleAction(actions.createSetupKey.request, () => initialState.createdSetupKey)
.handleAction(actions.createSetupKey.success, (store, action) => action.payload)
.handleAction(actions.createSetupKey.failure, (store, action) => action.payload)
.handleAction(actions.setCreateSetupKey, (store, action) => action.payload)
const setupNewKeyVisible = createReducer<boolean, ActionTypes>(initialState.setupNewKeyVisible)
.handleAction(actions.setSetupNewKeyVisible, (store, action) => action.payload)
export default combineReducers({
data,
setupKey,
loading,
failed,
saving,
deletedSetupKey,
revokedSetupKey,
createdSetupKey,
setupNewKeyVisible
});

View File

@@ -0,0 +1,153 @@
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, ChangeResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
import {SetupKey, SetupKeyRevoke} from './types'
import service from './service';
import actions from './actions';
import {take} from "lodash";
export function* getSetupKeys(action: ReturnType<typeof actions.getSetupKeys.request>): Generator {
try {
yield put(actions.setDeleteSetupKey({
loading: false,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>))
const effect = yield call(service.getSetupKeys, action.payload);
const response = effect as ApiResponse<SetupKey[]>;
yield put(actions.getSetupKeys.success(response.body));
} catch (err) {
yield put(actions.getSetupKeys.failure(err as ApiError));
}
}
export function* setCreateSetupKey(action: ReturnType<typeof actions.setCreateSetupKey>): Generator {
yield put(actions.setCreateSetupKey(action.payload))
}
export function* createSetupKey(action: ReturnType<typeof actions.createSetupKey.request>): Generator {
try {
yield put(actions.setCreateSetupKey({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as CreateResponse<SetupKey | null>))
const effect = yield call(service.createSetupKey, action.payload);
const response = effect as ApiResponse<SetupKey>;
yield put(actions.createSetupKey.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as CreateResponse<SetupKey | null>));
const setupKeys = [...(yield select(state => state.setupKey.data)) as SetupKey[]]
setupKeys.unshift(response.body)
yield put(actions.getSetupKeys.success(setupKeys));
} catch (err) {
yield put(actions.createSetupKey.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as CreateResponse<SetupKey | null>));
}
}
export function* setDeleteSetupKey(action: ReturnType<typeof actions.setDeleteSetupKey>): Generator {
yield put(actions.setDeleteSetupKey(action.payload))
}
export function* deleteSetupKey(action: ReturnType<typeof actions.deleteSetupKey.request>): Generator {
try {
yield call(actions.setDeleteSetupKey,{
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
const effect = yield call(service.deleteSetupKey, action.payload);
const response = effect as ApiResponse<any>;
yield put(actions.deleteSetupKey.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
const setupKeys = (yield select(state => state.setupKey.data)) as SetupKey[]
yield put(actions.getSetupKeys.success(setupKeys.filter((p:SetupKey) => p.Id !== action.payload.payload)))
} catch (err) {
yield put(actions.deleteSetupKey.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as DeleteResponse<string | null>));
}
}
export function* revokeSetupKey(action: ReturnType<typeof actions.revokeSetupKey.request>): Generator {
try {
yield put(actions.setRevokeSetupKey({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as ChangeResponse<SetupKey | null>))
const effect = yield call(service.revokeSetupKey, action.payload);
const response = effect as ApiResponse<SetupKey>;
yield put(actions.revokeSetupKey.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as ChangeResponse<SetupKey | null>));
const setupKeys = [...(yield select(state => state.setupKey.data)) as SetupKey[]]
let setupKey = setupKeys.find(s => s.Id === response.body.Id) as SetupKey
if (setupKey) {
setupKey.Revoked = response.body.Revoked
setupKey.Valid = response.body.Valid
setupKey.State = response.body.State
setupKey.Expires = response.body.Expires
}
yield put(actions.getSetupKeys.success(setupKeys));
} catch (err) {
yield put(actions.createSetupKey.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as CreateResponse<SetupKey | null>));
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getSetupKeys.request, getSetupKeys),
takeLatest(actions.createSetupKey.request, createSetupKey),
takeLatest(actions.deleteSetupKey.request, deleteSetupKey),
takeLatest(actions.revokeSetupKey.request, revokeSetupKey)
]);
}

View File

@@ -0,0 +1,36 @@
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import {SetupKey, SetupKeyNew, SetupKeyRevoke} from './types';
export default {
async getSetupKeys(payload:RequestPayload<null>): Promise<ApiResponse<SetupKey[]>> {
return apiClient.get<SetupKey[]>(
`/api/setup-keys`,
payload
);
},
async deleteSetupKey(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`/api/setup-keys/` + payload.payload,
payload
);
},
async revokeSetupKey(payload:RequestPayload<SetupKeyRevoke>): Promise<ApiResponse<SetupKey>> {
return apiClient.put<SetupKey>(
`/api/setup-keys/` + payload.payload.Id,
payload
);
},
async renameSetupKey(payload:RequestPayload<any>): Promise<ApiResponse<SetupKey>> {
return apiClient.put<SetupKey>(
`/api/setup-keys/` + payload.payload.Id,
payload
);
},
async createSetupKey(payload:RequestPayload<SetupKey>): Promise<ApiResponse<SetupKey>> {
return apiClient.post<SetupKey>(
`/api/setup-keys`,
payload
);
},
};

View File

@@ -0,0 +1,23 @@
export interface SetupKey {
Expires: string;
Id: string;
Key: string;
LastUsed: string;
Name: string;
Revoked: boolean;
State: string;
Type: string;
UsedTimes: number;
Valid: boolean;
}
export interface SetupKeyNew {
Id: string;
Name: string;
Type: string;
}
export interface SetupKeyRevoke {
Id: string;
Revoked: boolean;
}

12
src/store/types.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
import { StateType, ActionType } from 'typesafe-actions';
declare module 'typesafe-actions' {
export type RootState = StateType<
ReturnType<typeof import('./root-reducer').default>
>;
export type RootAction = ActionType<typeof import('./root-action').default>;
interface Types {
RootAction: RootAction;
}
}

14
src/store/user/actions.ts Normal file
View File

@@ -0,0 +1,14 @@
import { ActionType, createAsyncAction } from 'typesafe-actions';
import { User } from './types';
import { ApiError, RequestPayload } from '../../services/api-client/types';
const actions = {
getUsers: createAsyncAction(
'GET_USERS_REQUEST',
'GET_USERS_SUCCESS',
'GET_USERS_FAILURE',
)<RequestPayload<null>, User[], ApiError>()
};
export type ActionTypes = ActionType<typeof actions>;
export default actions;

7
src/store/user/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import actions, { ActionTypes as _actionTypes } from './actions';
import reducer from './reducer';
import sagas from './sagas';
export type ActionTypes = _actionTypes;
export { actions, reducer, sagas };

38
src/store/user/reducer.ts Normal file
View File

@@ -0,0 +1,38 @@
import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { User } from './types';
import actions, { ActionTypes } from './actions';
import { ApiError } from "../../services/api-client/types";
type StateType = Readonly<{
data: User[] | null;
loading: boolean;
failed: ApiError | null;
}>;
const initialState: StateType = {
data: [],
loading: false,
failed: null,
};
const data = createReducer<User[], ActionTypes>(initialState.data as User[])
.handleAction(actions.getUsers.success,(_, action) => action.payload)
.handleAction(actions.getUsers.failure, () => []);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getUsers.request, () => true)
.handleAction(actions.getUsers.success, () => false)
.handleAction(actions.getUsers.failure, () => false);
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
.handleAction(actions.getUsers.request, () => null)
.handleAction(actions.getUsers.success, () => null)
.handleAction(actions.getUsers.failure, (store, action) => action.payload);
export default combineReducers({
data,
loading,
failed
});

23
src/store/user/sagas.ts Normal file
View File

@@ -0,0 +1,23 @@
import {all, call, put, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse} from '../../services/api-client/types';
import { User } from './types'
import service from './service';
import actions from './actions';
export function* getPeers(action: ReturnType<typeof actions.getUsers.request>): Generator {
try {
const effect = yield call(service.getUsers, action.payload);
const response = effect as ApiResponse<User[]>;
yield put(actions.getUsers.success(response.body));
} catch (err) {
yield put(actions.getUsers.failure(err as ApiError));
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getUsers.request, getPeers)
]);
}

12
src/store/user/service.ts Normal file
View File

@@ -0,0 +1,12 @@
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import { User } from './types';
export default {
async getUsers(payload:RequestPayload<null>): Promise<ApiResponse<User[]>> {
return apiClient.get<User[]>(
`/api/users`,
payload
);
}
};

6
src/store/user/types.ts Normal file
View File

@@ -0,0 +1,6 @@
export interface User {
id: string;
email?: string;
name: string;
role: string;
}

4
src/types/images.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.png';
declare module '*.svg';

View File

@@ -1,3 +1,15 @@
export const formatOS = (os) => {
if (os.startsWith("windows 10")) {
return "Windows 10";
}
if (os.startsWith("Darwin")) {
return os.replace("Darwin", "MacOS");
}
return os;
};
export const formatDate = date => {
return new Date(date).toLocaleDateString("en-GB", { weekday: 'short', year: '2-digit', month: 'short', day: 'numeric' });
}
@@ -71,4 +83,8 @@ export const timeAgo = (dateParam) => {
}
return getFormattedDate(date); // 10. January 2017. at 10:20
}
export const copyToClipboard = (copyText) => {
navigator.clipboard.writeText(copyText);
}

View File

@@ -1,59 +0,0 @@
import React, {useState} from "react";
import {withAuthenticationRequired} from "@auth0/auth0-react";
import Loading from "../components/Loading";
export const AccessControlComponent = () => {
const [error] = useState(null)
return (
<>
<div className="py-10">
<header>
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-xl font-semibold text-gray-900">Access Control</h1>
</div>
</header>
<main>
<div className="max-w-5xl mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-8 sm:px-0">
{error != null && (
<span>{error.toString()}</span>
)}
<h1 className="text-m leading-tight text-gray-900 font-bold">
Create and control access groups
</h1>
<br/>
<p className="text-sm">
Here you will be able to specify what peers or groups of peers are able to connect to
each other.
For example, you might have 3 departments in your organization - IT, HR, Finance.
In most cases Finance and HR departments wouldn't need to access machines of the IT
department.
In such scenario you could create 3 separate tags (groups) and label peers accordingly
so that only
peers
from the same group can access each other.
You could also specify what groups can connect to each other and do fine grained control
even on a
peer level.
</p>
<br/>
<p className="text-sm">Stay tuned.</p>
</div>
</div>
</main>
</div>
</>
);
}
;
export default withAuthenticationRequired(AccessControlComponent,
{
onRedirecting: () => <Loading/>,
}
);

396
src/views/AccessControl.tsx Normal file
View File

@@ -0,0 +1,396 @@
import React, {useEffect, useState} from 'react';
import {useAuth0, withAuthenticationRequired} from "@auth0/auth0-react";
import {
Alert,
Button, Card,
Col, Dropdown, Input, Menu, message, Modal, Popover, Radio, RadioChangeEvent,
Row, Select, Space, Table, Tag,
Typography
} from "antd";
import {Container} from "../components/Container";
import Loading from "../components/Loading";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {Rule} from "../store/rule/types";
import {actions as ruleActions} from "../store/rule";
import {actions as groupActions} from "../store/group";
import {filter, sortBy} from "lodash";
import {CloseOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
import bidirect from '../assets/direct_bi.svg';
import inbound from '../assets/direct_in.svg';
import outbound from '../assets/direct_out.svg';
import tutorial from "../assets/access_control_tutorial.svg";
import AccessControlNew from "../components/AccessControlNew";
import {Group} from "../store/group/types";
import {actions as setupKeyActions} from "../store/setup-key";
import AccessControlModalGroups from "../components/AccessControlModalGroups";
const { Title, Paragraph } = Typography;
const { Column } = Table;
const { confirm } = Modal;
interface RuleDataTable extends Rule {
key: string;
sourceCount: number;
sourceLabel: '';
destinationCount: number;
destinationLabel: '';
}
interface GroupsToShow {
title: string,
groups: Group[] | string[] | null,
modalVisible: boolean
}
export const AccessControl = () => {
const { getAccessTokenSilently } = useAuth0()
const dispatch = useDispatch()
const rules = useSelector((state: RootState) => state.rule.data);
const failed = useSelector((state: RootState) => state.rule.failed);
const loading = useSelector((state: RootState) => state.rule.loading);
const deletedRule = useSelector((state: RootState) => state.rule.deletedRule);
const savedRule = useSelector((state: RootState) => state.rule.savedRule);
const [showTutorial, setShowTutorial] = useState(true)
const [textToSearch, setTextToSearch] = useState('');
const [optionAllEnable, setOptionAllEnable] = useState('all');
const [pageSize, setPageSize] = useState(5);
const [currentPage, setCurrentPage] = useState(1);
const [dataTable, setDataTable] = useState([] as RuleDataTable[]);
const [ruleToAction, setRuleToAction] = useState(null as RuleDataTable | null);
const [groupsToShow, setGroupsToShow] = useState({} as GroupsToShow)
const pageSizeOptions = [
{label: "5", value: "5"},
{label: "10", value: "10"},
{label: "15", value: "15"}
]
const optionsAllEnabled = [{label: 'All', value: 'all'}, {label: 'Enabled', value: 'enabled'}]
const itemsMenuAction = [
{
key: "view",
label: (<Button type="text" block onClick={() => onClickViewRule()}>View</Button>)
},
// {
// key: "delete",
// label: (<Button type="text" block onClick={() => showConfirmDeactivate()}>Deactivate</Button>)
// },
{
key: "delete",
label: (<Button type="text" block onClick={() => showConfirmDelete()}>Delete</Button>)
}
]
const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>)
const getSourceDestinationLabel = (data:Group[]):string => {
return (!data) ? "No group" : (data.length > 1) ? `${data.length} Groups` : (data.length === 1) ? data[0].Name : "No group"
}
const isShowTutorial = (rules:Rule[]):boolean => {
return (!rules.length || (rules.length === 1 && rules[0].Name === "Default"))
}
const transformDataTable = (d:Rule[]):RuleDataTable[] => {
return d.map(p => {
const sourceLabel = getSourceDestinationLabel(p.Source as Group[])
const destinationLabel = getSourceDestinationLabel(p.Destination as Group[])
return {
key: p.ID, ...p,
sourceCount: p.Source?.length,
sourceLabel,
destinationCount: p.Destination?.length,
destinationLabel
} as RuleDataTable
})
}
useEffect(() => {
dispatch(ruleActions.getRules.request({getAccessTokenSilently, payload: null}));
dispatch(groupActions.getGroups.request({getAccessTokenSilently, payload: null}));
}, [])
useEffect(() => {
setShowTutorial(isShowTutorial(rules))
setDataTable(sortBy(transformDataTable(rules), "Name"))
}, [rules])
useEffect(() => {
setDataTable(transformDataTable(filterDataTable()))
}, [textToSearch, optionAllEnable])
const styleNotification = { marginTop: 85 }
const saveKey = 'saving';
useEffect(() => {
if (savedRule.loading) {
message.loading({ content: 'Saving...', key: saveKey, duration: 0, style: styleNotification });
} else if (savedRule.success) {
message.success({ content: 'Rule saved with success!', key: saveKey, duration: 2, style: styleNotification });
dispatch(ruleActions.setSetupNewRuleVisible(false));
dispatch(ruleActions.setSavedRule({ ...savedRule, success: false }));
} else if (savedRule.error) {
message.error({ content: 'Error! Something wrong to create key.', key: saveKey, duration: 2, style: styleNotification });
dispatch(ruleActions.setSavedRule({ ...savedRule, error: null }));
}
}, [savedRule])
const deleteKey = 'deleting';
useEffect(() => {
const style = { marginTop: 85 }
if (deletedRule.loading) {
message.loading({ content: 'Deleting...', key: deleteKey, style });
} else if (deletedRule.success) {
message.success({ content: 'Rule deleted with success!', key: deleteKey, duration: 2, style });
} else if (deletedRule.error) {
message.error({ content: 'Error! Something wrong to delete rule.', key: deleteKey, duration: 2, style });
}
}, [deletedRule])
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setTextToSearch(e.target.value)
};
const searchDataTable = () => {
const data = filterDataTable()
setDataTable(transformDataTable(data))
}
const onChangeAllEnabled = ({ target: { value } }: RadioChangeEvent) => {
setOptionAllEnable(value)
}
const onChangePageSize = (value: string) => {
setPageSize(parseInt(value.toString()))
}
const showConfirmDelete = () => {
confirm({
icon: <ExclamationCircleOutlined />,
width: 600,
content: <Space direction="vertical" size="small">
{ruleToAction &&
<>
<Title level={5}>Delete rule "{ruleToAction ? ruleToAction.Name : ''}"</Title>
<Paragraph>Are you sure you want to delete peer from your account?</Paragraph>
</>
}
</Space>,
okType: 'danger',
onOk() {
dispatch(ruleActions.deleteRule.request({getAccessTokenSilently, payload: ruleToAction?.ID || ''}));
},
onCancel() {
setRuleToAction(null);
},
});
}
const showConfirmDeactivate = () => {
confirm({
icon: <ExclamationCircleOutlined />,
width: 600,
content: <Space direction="vertical" size="small">
{ruleToAction &&
<>
<Title level={5}>Deactivate rule "{ruleToAction ? ruleToAction.Name : ''}"</Title>
<Paragraph>Are you sure you want to deactivate peer from your account?</Paragraph>
</>
}
</Space>,
okType: 'danger',
onOk() {
//dispatch(ruleActions.deleteRule.request({getAccessTokenSilently, payload: ruleToAction?.ID || ''}));
},
onCancel() {
setRuleToAction(null);
},
});
}
const filterDataTable = ():Rule[] => {
const t = textToSearch.toLowerCase().trim()
let f:Rule[] = filter(rules, (f:Rule) =>
(f.Name.toLowerCase().includes(t) || t === "")
) as Rule[]
// if (optionAllEnabled === "enabled") {
// f = filter(rules, (f:Rule) => f.)
// }
return f
}
const onClickAddNewRule = () => {
dispatch(ruleActions.setSetupNewRuleVisible(true));
dispatch(ruleActions.setRule({
Name: '',
Source: [],
Destination: [],
Flow: 'bidirect'
} as Rule))
}
const onClickViewRule = () => {
dispatch(ruleActions.setSetupNewRuleVisible(true));
dispatch(ruleActions.setRule({
ID: ruleToAction?.ID || null,
Name: ruleToAction?.Name,
Source: ruleToAction?.Source,
Destination: ruleToAction?.Destination,
Flow: ruleToAction?.Flow
} as Rule))
}
const toggleModalGroups = (title:string, groups:Group[] | string[] | null, modalVisible:boolean) => {
setGroupsToShow({
title,
groups,
modalVisible
})
}
const renderPopoverGroups = (label: string, groups:Group[] | string[] | null) => {
const content = groups?.map(g => {
const _g = g as Group
const peersCount = ` - ${_g.PeersCount || 0} ${(_g.PeersCount && parseInt(_g.PeersCount) > 1) ? 'peers' : 'peer'} `
return (
<div>
<Tag
color="blue"
style={{ marginRight: 3 }}
>
<strong>{_g.Name}</strong>
</Tag>
<span style={{fontSize: ".85em"}}>{peersCount}</span>
</div>
)
})
return (
<Popover content={<Space direction="vertical">{content}</Space>} title={null}>
<Button type="link">{label}</Button>
</Popover>
)
}
return(
<>
<Container className="container-main">
<Row>
<Col span={24}>
<Title level={4}>Access Control</Title>
<Paragraph>Create and control access groups</Paragraph>
<Space direction="vertical" size="large" style={{ display: 'flex' }}>
<Row gutter={[16, 24]}>
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
<Input allowClear value={textToSearch} onPressEnter={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />
</Col>
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
<Space size="middle">
{/*<Radio.Group
options={optionsAllEnabled}
onChange={onChangeAllEnabled}
value={optionAllEnable}
optionType="button"
buttonStyle="solid"
/>*/}
<Select value={pageSize.toString()} options={pageSizeOptions} onChange={onChangePageSize} className="select-rows-per-page-en"/>
</Space>
</Col>
<Col xs={24}
sm={24}
md={5}
lg={5}
xl={5}
xxl={5} span={5}>
<Row justify="end">
<Col>
<Button type="primary" disabled={savedRule.loading} onClick={onClickAddNewRule}>Add Rule</Button>
</Col>
</Row>
</Col>
</Row>
{failed &&
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
}
{loading && <Loading/>}
<Card bodyStyle={{padding: 0}}>
<Table
pagination={{
current: currentPage, hideOnSinglePage: showTutorial, disabled: showTutorial,
pageSize, responsive: true, showSizeChanger: false,
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} rules`),
onChange: (page, pageSize) => {
setCurrentPage(page)
}
}}
className={`${showTutorial ? "card-table card-table-no-placeholder" : "card-table"}`}
showSorterTooltip={false}
scroll={{x: true}}
dataSource={dataTable}>
<Column title="Name" dataIndex="Name"
onFilter={(value: string | number | boolean, record) => (record as any).Name.includes(value)}
sorter={(a, b) => ((a as any).Name.localeCompare((b as any).Name))} />
<Column title="Sources" dataIndex="sourceLabel"
render={(text, record:RuleDataTable, index) => {
//return <Button type="link" onClick={() => toggleModalGroups(`${record.Name} - Sources`, record.Source, true)}>{text}</Button>
return renderPopoverGroups(text, record.Source)
}}
/>
<Column title="Direction" dataIndex="Flow"
render={(text, record:RuleDataTable, index) => {
const s = {minWidth: 50, textAlign: "center"} as React.CSSProperties
if (text === "bidirect")
return <Tag color="processing" style={s}><img src={bidirect}/></Tag>
else if (text === "srcToDest") {
return <Tag color="green" style={s}><img src={outbound}/></Tag>
} else if (text === "destToSrc") {
return <Tag color="green" style={s}><img src={inbound}/></Tag>
}
return <Tag color="red" style={s}><CloseOutlined /></Tag>
}}
/>
<Column title="Destinations" dataIndex="destinationLabel"
render={(text, record:RuleDataTable, index) => {
//return <Button type="link" onClick={() => toggleModalGroups(`${record.Name} - Destinations`, record.Destination, true)}>{text}</Button>
return renderPopoverGroups(text, record.Destination)
}}
/>
<Column title="" align="center"
render={(text, record, index) => {
if (dataTable.length === 1 || deletedRule.loading || savedRule.loading) return <></>
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
onVisibleChange={visible => {
if (visible) setRuleToAction(record as RuleDataTable)
}}></Dropdown.Button>
}}
/>
</Table>
{showTutorial &&
<Space direction="vertical" size="small" align="center"
style={{display: 'flex', padding: '45px 15px'}}>
<img src={tutorial} style={{width: 362, paddingBottom: 45}}/>
<Title level={5}>Create and control access groups</Title>
<Paragraph>
Access rules help you manage access permissions in your organisation.
</Paragraph>
<Button type="link" onClick={onClickAddNewRule}>Add new access rule</Button>
</Space>
}
</Card>
</Space>
</Col>
</Row>
</Container>
<AccessControlModalGroups data={groupsToShow.groups} title={groupsToShow.title} visible={groupsToShow.modalVisible} onCancel={() => toggleModalGroups("", [], false)}/>
<AccessControlNew/>
</>
)
}
export default withAuthenticationRequired(AccessControl,
{
onRedirecting: () => <Loading/>,
}
);

View File

@@ -1,47 +0,0 @@
import React, {useState} from "react";
import {withAuthenticationRequired} from "@auth0/auth0-react";
import Loading from "../components/Loading";
export const ActivityComponent = () => {
const [error] = useState(null)
return (
<>
<div className="py-10">
<header>
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-xl font-semibold text-gray-900">Activity</h1>
</div>
</header>
<main>
<div className="max-w-5xl mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-8 sm:px-0">
{error != null && (
<span>{error.toString()}</span>
)}
<h1 className="text-m leading-tight text-gray-900 font-bold">
Monitor system activity.
</h1>
<br/>
<p className="text-sm">
Here you will be able to see activity of peers. E.g. events like Peer A has connected to Peer B
</p>
<br/>
<p className="text-sm">Stay tuned.</p>
</div>
</div>
</main>
</div>
</>
);
}
;
export default withAuthenticationRequired(ActivityComponent,
{
onRedirecting: () => <Loading/>,
}
);

36
src/views/Activity.tsx Normal file
View File

@@ -0,0 +1,36 @@
import React from 'react';
import {withAuthenticationRequired} from "@auth0/auth0-react";
import {
Col,
Row,
Typography
} from "antd";
import {Container} from "../components/Container";
import Loading from "../components/Loading";
const { Title, Paragraph } = Typography;
export const Activity = () => {
return(
<Container style={{paddingTop: "40px"}}>
<Row>
<Col span={24}>
<Title level={4}>Activity</Title>
<Title level={5}>Monitor system activity.</Title>
<Paragraph>
Here you will be able to see activity of peers. E.g. events like Peer A has connected to Peer B.
</Paragraph>
<Paragraph>
Stay tuned.
</Paragraph>
</Col>
</Row>
</Container>
)
}
export default withAuthenticationRequired(Activity,
{
onRedirecting: () => <Loading/>,
}
);

View File

@@ -1,69 +0,0 @@
import React, {useEffect, useState} from "react";
import {useAuth0, withAuthenticationRequired} from "@auth0/auth0-react";
import Loading from "../components/Loading";
import {getSetupKeys} from "../api/ManagementAPI";
import AddPeerTabSelector from "../components/addpeer/AddPeerTabSelector";
import SetupKeySelect from "../components/addpeer/SetupKeySelect";
export const AddPeerComponent = () => {
const [setupKeys, setSetupKeys] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [selectedKey, setSelectedKey] = useState(null)
const {
getAccessTokenSilently,
} = useAuth0();
const handleError = error => {
console.error('Error to fetch data:', error);
setLoading(false)
setError(error);
};
useEffect(() => {
getSetupKeys(getAccessTokenSilently)
.then(responseData => setSetupKeys(responseData))
.then(() => setLoading(false))
.catch(error => handleError(error))
}, [getAccessTokenSilently])
return (
<>
<div className="py-10 bg-gray-50 overflow-hidden rounded max-w-7xl mx-auto sm:px-6 lg:px-8">
<header className="sm:flex sm:items-center">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 sm:flex-auto">
<h1 className="text-xl font-semibold text-gray-900">Add Peer</h1>
<p className="mt-2 text-sm text-gray-700">
To get started with Netbird just install the app and log in.
</p>
</div>
</header>
<main>
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-4 sm:px-0">
{loading && (<Loading/>)}
{error != null && (
<span>{error.toString()}</span>
)}
{setupKeys && (<nav aria-label="Progress">
<AddPeerTabSelector setupKey={selectedKey}/>
</nav>)}
</div>
</div>
</main>
</div>
</>
);
}
;
export default withAuthenticationRequired(AddPeerComponent,
{
onRedirecting: () => <Loading/>,
}
);

62
src/views/AddPeer.tsx Normal file
View File

@@ -0,0 +1,62 @@
import React, {useEffect, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {useAuth0, withAuthenticationRequired} from "@auth0/auth0-react";
import Loading from "../components/Loading";
import {Container} from "../components/Container";
import {
Col,
Row,
Typography,
Space,
Tabs
} from "antd";
import {ExclamationCircleOutlined} from "@ant-design/icons";
import LinuxTab from "../components/addpeer/LinuxTab";
import MacTab from "../components/addpeer/MacTab";
import WindowsTab from "../components/addpeer/WindowsTab";
const { Title, Paragraph } = Typography;
const { TabPane } = Tabs;
export const AddPeer = () => {
const { getAccessTokenSilently } = useAuth0()
const dispatch = useDispatch()
useEffect(() => {
}, [])
const onChangeTab = (key: string) => {};
return (
<>
<Container style={{paddingTop: "40px"}}>
<Row>
<Col span={24}>
<Title level={4}>Add Peer</Title>
<Paragraph>To get started with NetBird just install the app and log in.</Paragraph>
<Space direction="vertical" size="large" style={{ display: 'flex' }}>
<Tabs onChange={onChangeTab} tabPosition="top" animated={{ inkBar: true, tabPane: false }}>
<TabPane tab="Linux" key="1">
<LinuxTab/>
</TabPane>
<TabPane tab="Windows" key="2">
<WindowsTab/>
</TabPane>
<TabPane tab="MacOS" key="3">
<MacTab/>
</TabPane>
</Tabs>
</Space>
</Col>
</Row>
</Container>
</>
)
}
export default withAuthenticationRequired(AddPeer,
{
onRedirecting: () => <Loading/>,
}
)

View File

@@ -1,550 +0,0 @@
import { useAuth0, withAuthenticationRequired } from "@auth0/auth0-react";
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
import React, { useEffect, useState } from "react";
// import PaginatedPeersList from "../components/PaginatedPeersList"
import { Link } from "react-router-dom";
import { usePagination, useTable } from "react-table";
import { deletePeer, getPeers } from "../api/ManagementAPI";
import CopyText from "../components/CopyText";
import DeleteModal from "../components/DeleteDialog";
import EditButton from "../components/EditButton";
import EmptyPeersPanel from "../components/EmptyPeers";
import Loading from "../components/Loading";
import { timeAgo } from "../utils/common";
export const Peers = () => {
const [peers, setPeers] = useState([]);
const [peersBackUp, setPeersBackUp] = useState([]);
const [empty, setEmpty] = useState(true);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [peerToDelete, setPeerToDelete] = useState(null);
const [deleteDialogText, setDeleteDialogText] = useState("");
const [deleteDialogTitle, setDeleteDialogTitle] = useState("");
const { getAccessTokenSilently } = useAuth0();
const handleError = (error) => {
console.error("Error to fetch data:", error);
setLoading(false);
setError(error);
};
// Add React Table
const data = React.useMemo(() => peers, [peers]);
const columns = React.useMemo(
() => [
{
Header: "Name",
accessor: "Name",
},
{
Header: "IP",
accessor: "IP",
},
{
Header: "Status",
accessor: "Connected",
},
{
Header: "Last Seen",
accessor: "LastSeen",
},
{
Header: "OS",
accessor: "OS",
},
{
Header: "Version",
accessor: "Version",
},
],
[]
);
const td_class_name =
"whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6";
const td_class_other = "whitespace-nowrap px-3 py-4 text-sm text-gray-500";
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageCount,
gotoPage,
nextPage,
previousPage,
state: { pageIndex, pageSize },
} = useTable(
{ columns, data, initialState: { pageIndex: 0, pageSize: 5 } },
usePagination
);
const handleSearch = (e) => {
let tempArray = peersBackUp.filter((item) => {
return item.Name.toUpperCase().includes(e.toUpperCase()) || item.IP.toUpperCase().includes(e.toUpperCase())
});
setPeers(tempArray);
};
const sortTable = (e) => {
let peerCopy = [...peers];
if (e === "0") {
peerCopy.sort((a, b) => (a.Name > b.Name ? 1 : -1));
} else if (e === "1") {
peerCopy.sort((a, b) => (a.Name > b.Name ? -1 : 1));
} else if (e === "2") {
peerCopy.sort((a, b) => (a.LastSeen > b.LastSeen ? 1 : -1));
} else if (e === "3") {
peerCopy.sort((a, b) => (a.LastSeen > b.LastSeen ? -1 : 1));
} else {
console.log(`Sorry, we are out of ${e}`, e);
}
setPeers(peerCopy);
};
const InnerPageNumbers = () => {
let default_btn =
"z-10 bg-white border-gray-300 text-gray-700 relative inline-flex items-center px-4 py-2 border hover:bg-gray-50";
let clicked_btn =
"z-10 bg-gray-50 border-gray-500 text-gray-600 relative inline-flex items-center px-4 py-2 border hover:bg-gray-50";
let menuItems = [];
if (pageCount < 6) {
for (let i = 0; i < pageCount; i++) {
menuItems.push(
<button
className={pageIndex === i ? clicked_btn : default_btn}
onClick={() => gotoPage(i)}
>
{i + 1}
</button>
);
}
} else {
let j =
pageIndex === 0 || pageIndex === 1
? 0
: pageCount - pageIndex === 1 ||
pageCount - pageIndex === 0 ||
pageCount - pageIndex === 2
? pageCount - 5
: pageIndex - 2;
for (let i = j; i < j + 5; i++) {
menuItems.push(
<button
className={pageIndex === i ? clicked_btn : default_btn}
onClick={() => gotoPage(i)}
>
{i + 1}
</button>
);
}
}
return <div>{menuItems}</div>;
};
const formatOS = (os) => {
if (os.startsWith("windows 10")) {
return "Windows 10";
}
if (os.startsWith("Darwin")) {
return os.replace("Darwin", "MacOS");
}
return os;
};
//called when user clicks on table row menu item
const handleRowMenuClick = (action, peer) => {
if (action === "Delete") {
setPeerToDelete(peer[1].value);
setDeleteDialogText(
"Are you sure you want to delete peer from your account?"
);
setDeleteDialogTitle('Delete peer "' + peer[0].value + '"');
setShowDeleteDialog(true);
}
};
const showAll = () => {
const showAllBtn = document.getElementById("btn-show-all");
const showOnlineBtn = document.getElementById("btn-show-online");
showAllBtn.classList.add(
"ring-1",
"ring-indigo-500",
"border-indigo-500",
"outline-none"
);
showOnlineBtn.classList.remove(
"ring-1",
"ring-indigo-500",
"border-indigo-500",
"outline-none"
);
refresh(null);
};
const showConnected = () => {
const showAllBtn = document.getElementById("btn-show-all");
const showOnlineBtn = document.getElementById("btn-show-online");
showOnlineBtn.classList.add(
"ring-1",
"ring-indigo-500",
"border-indigo-500",
"outline-none"
);
showAllBtn.classList.remove(
"ring-1",
"ring-indigo-500",
"border-indigo-500",
"outline-none"
);
refresh(function (peers) {
return peers.filter((peer) => {
return peer.Connected;
});
});
};
const refresh = (filter) => {
getPeers(getAccessTokenSilently)
.then((responseData) =>
responseData.sort((a, b) => (a.Name > b.Name ? 1 : -1))
)
.then((list) => {
setEmpty(list.length === 0);
return list;
})
.then((sorted) => {
return filter != null ? filter(sorted) : sorted;
})
.then((filtered) => {
setPeersBackUp(filtered);
setPeers(filtered);
})
.then(() => setLoading(false))
.catch((error) => handleError(error));
};
// after user confirms (or not) deletion of the peer
const handleDeleteConfirmation = (confirmed) => {
setShowDeleteDialog(false);
if (confirmed) {
deletePeer(getAccessTokenSilently, peerToDelete)
.then(() => setPeerToDelete(null))
.then(() => refresh(null))
.catch((error) => {
setPeerToDelete(null);
console.log(error);
});
} else {
setPeerToDelete(null);
}
};
useEffect(() => {
refresh(null);
}, [getAccessTokenSilently]);
useEffect(() => {}, [peers]);
return (
<div className="py-10 bg-gray-50 overflow-hidden rounded max-w-7xl mx-auto sm:px-6 lg:px-8">
<header className="sm:flex sm:items-center">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 sm:flex-auto">
<h1 className="text-xl font-semibold text-gray-900">Peers</h1>
<p className="mt-2 text-sm text-gray-700">
A list of all the machines in your account including their name, IP
and status.
</p>
</div>
</header>
<main>
<div className="max-w-7xl mx-auto">
<div className="px-4 sm:px-0">
{loading && <Loading />}
{error != null && <span>{error.toString()}</span>}
<main>
{loading && <Loading />}
{error != null && <span>{error.toString()}</span>}
{!empty ? (
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="auto py-8">
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-6 gap-5">
<div className="lg:col-span-2">
<input
className="text-sm w-full rounded p-2 border border-gray-300 focus:border-gray-400 outline-none"
placeholder="Search..."
type="search"
onChange={(e) => handleSearch(e.target.value)}
/>
</div>
<div className="lg:col-span-2">
<div className="flex items-center">
<p className="ml-0 text-sm text-gray-700 lg:px-4 md:pr-2 pr-2">Sort by: &nbsp;</p>
<select
className="bg-gray-50 flex-1 lg:flex-grow-0 text-sm text-gray-500 rounded p-2 border border-gray-300 focus:border-gray-400 outline-none"
onChange={(e) => sortTable(e.target.value)}
>
<option className="text-sm text-gray-500" value={0}>Name: Asc</option>
<option className="text-sm text-gray-500" value={1}>Name: Desc</option>
<option className="text-sm text-gray-500" value={2}>Last Seen: Asc</option>
<option className="text-sm text-gray-500" value={3}>Last Seen: Desc</option>
</select>
</div>
</div>
<div className="flex lg:justify-end justify-center">
<div className="flex items-center">
<span className="relative z-0 inline-flex shadow-sm rounded-md">
<button
id="btn-show-all"
onClick={() => showAll()}
type="button"
className="relative inline-flex items-center px-4 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 z-10 outline-none ring-1 ring-indigo-500 border-indigo-500"
>
All
</button>
<button
type="button"
id="btn-show-online"
onClick={() => showConnected()}
className="relative inline-flex items-center px-4 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-700 outline-none hover:bg-gray-50"
>
Online
</button>
</span>
</div>
</div>
<div className="lg:flex lg:justify-end">
<Link to="/add-peer">
<button
type="button"
className="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto"
>
Add peer
</button>
</Link>
</div>
</div>
</div>
<div className="px-4 py-2 sm:px-0">
<DeleteModal
show={showDeleteDialog}
confirmCallback={handleDeleteConfirmation}
text={deleteDialogText}
title={deleteDialogTitle}
/>
{/* table */}
<div className="flex flex-col">
<div className="-my-2 sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle min-w-full sm:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<div className="overflow-x-auto">
<table
{...getTableProps()}
className="min-w-full divide-y divide-gray-200"
>
<thead className="bg-gray-50">
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column, i) => (
<th
{...column.getHeaderProps()}
className={
i === 0
? "px-6 py-3.5 text-left text-sm font-semibold text-gray-900"
: "px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
}
>
{column.render("Header")}
</th>
))}
<th
scope="col"
className="relative px-6 py-3"
>
<span className="sr-only">Edit</span>
</th>
</tr>
))}
</thead>
<tbody
{...getTableBodyProps()}
className="bg-white divide-y divide-gray-200"
>
{page.map((row) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<td
{...cell.getCellProps()}
className={
cell.column.id === "Name"
? td_class_name
: td_class_other
}
>
{cell.column.id === "IP" && (
<CopyText
text={cell.value.toUpperCase()}
idPrefix={
"peers-ip-" + cell.value
}
/>
)}
{cell.column.id === "Connected" &&
(cell.value ? (
<span className="inline-flex rounded-full bg-green-100 px-2 text-xs leading-5 text-green-800">
online
</span>
) : (
<span className="inline-flex rounded-full bg-red-100 px-2 text-xs leading-5 text-red-800">
offline
</span>
))}
{cell.column.id === "LastSeen" &&
(cell.row.original.Connected
? "just now"
: timeAgo(cell.value))}
{cell.column.id === "OS" &&
formatOS(cell.value)}
{(cell.column.id === "Name" ||
cell.column.id === "Version") &&
cell.value}
</td>
);
})}
<td className={td_class_other}>
<div className="relative">
<EditButton
items={[{ name: "Delete" }]}
handler={(action) =>
handleRowMenuClick(
action,
row.cells
)
}
/>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* pagenation */}
<div className="bg-white px-4 py-3 flex items-center justify-center sm:justify-between border-t border-gray-200 sm:px-6">
<div className="sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-gray-700 text-center sm:text-left">
Showing{" "}
<span className="font-medium">
{pageCount === 0
? 0
: pageIndex * pageSize + 1}
</span>{" "}
to{" "}
<span className="font-medium">
{pageCount === 0
? 0
: pageIndex === pageCount - 1
? data.length
: pageIndex * pageSize + pageSize}
</span>{" "}
of{" "}
<span className="font-medium">
{data.length}
</span>{" "}
{data.length === 1 ? "peer" : "peers"}
</p>
</div>
{pageCount === 1 || pageCount === 0 ? (
<div />
) : (
<div>
<nav
className="py-3 relative z-0 inline-flex rounded-md shadow-sm -space-x-px"
aria-label="Pagination"
>
<button
className="relative inline-flex rounded-l-md items-center px-2 py-2 border border-gray-300 bg-white text-gray-500 hover:bg-gray-50"
onClick={() => gotoPage(0)}
disabled={!canPreviousPage}
>
First
</button>
<button
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-gray-500 hover:bg-gray-50"
onClick={() => previousPage()}
disabled={!canPreviousPage}
>
<span className="sr-only">
Previous
</span>
<ChevronLeftIcon
className="h-5 w-5"
aria-hidden="true"
/>
</button>
<div>
<InnerPageNumbers />
</div>
<button
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-gray-500 hover:bg-gray-50"
onClick={() => nextPage()}
disabled={!canNextPage}
>
<span className="sr-only">Next</span>
<ChevronRightIcon
className="h-5 w-5"
aria-hidden="true"
/>
</button>
<button
className="relative inline-flex rounded-r-md items-center px-2 py-2 border border-gray-300 bg-white text-gray-500 hover:bg-gray-50"
onClick={() => gotoPage(pageCount - 1)}
disabled={!canNextPage}
>
Last
</button>
</nav>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
) : (
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8 py-10">
<EmptyPeersPanel />
</div>
)}
</main>
</div>
</div>
</main>
</div>
);
};
export default withAuthenticationRequired(Peers, {
onRedirecting: () => <Loading />,
});

257
src/views/Peers.tsx Normal file
View File

@@ -0,0 +1,257 @@
import React, {useEffect, useState} from 'react';
import {Link} from 'react-router-dom';
import {useDispatch, useSelector} from "react-redux";
import {useAuth0, withAuthenticationRequired} from "@auth0/auth0-react";
import { RootState } from "typesafe-actions";
import { actions as peerActions } from '../store/peer';
import { actions as groupActions } from '../store/group';
import Loading from "../components/Loading";
import {Container} from "../components/Container";
import {
Col,
Row,
Typography,
Table,
Card,
Tag,
Input,
Space,
Radio,
RadioChangeEvent,
Dropdown,
Menu,
Alert, Select, Modal, Button, message
} from "antd";
import {Peer} from "../store/peer/types";
import {filter} from "lodash"
import {formatOS, timeAgo} from "../utils/common";
import {ExclamationCircleOutlined} from "@ant-design/icons";
import ButtonCopyMessage from "../components/ButtonCopyMessage";
import {Group} from "../store/group/types";
const { Title, Paragraph } = Typography;
const { Column } = Table;
const { confirm } = Modal;
interface PeerDataTable extends Peer {
key: string
}
export const Peers = () => {
const { getAccessTokenSilently } = useAuth0()
const dispatch = useDispatch()
const peers = useSelector((state: RootState) => state.peer.data);
const failed = useSelector((state: RootState) => state.peer.failed);
const loading = useSelector((state: RootState) => state.peer.loading);
const deletedPeer = useSelector((state: RootState) => state.peer.deletedPeer);
const groups = useSelector((state: RootState) => state.group.data);
const loadingGroups = useSelector((state: RootState) => state.group.loading);
const [textToSearch, setTextToSearch] = useState('');
const [optionOnOff, setOptionOnOff] = useState('all');
const [pageSize, setPageSize] = useState(5);
const [dataTable, setDataTable] = useState([] as PeerDataTable[]);
const [peerToAction, setPeerToAction] = useState(null as PeerDataTable | null);
const pageSizeOptions = [
{label: "5", value: "5"},
{label: "10", value: "10"},
{label: "15", value: "15"}
]
const optionsOnOff = [{label: 'All', value: 'all'}, {label: 'Online', value: 'on'}]
const itemsMenuAction = [
{
key: "delete",
label: (<Button type="text" onClick={() => showConfirmDelete()}>Delete</Button>)
}
]
const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>)
const transformDataTable = (d:Peer[]):PeerDataTable[] => {
return d.map(p => ({ key: p.IP, ...p } as PeerDataTable))
}
useEffect(() => {
dispatch(peerActions.getPeers.request({getAccessTokenSilently, payload: null}));
dispatch(groupActions.getGroups.request({getAccessTokenSilently, payload: null}));
// dispatch(groupActions.saveGroup.request({getAccessTokenSilently, payload: {
// ID: "caakdnhvdm4s73ak8cgg",
// Name: "ZZZZ",
// Peers: ["wksAVnZSc/ewyU8f8UFqQjjb3TKOqgxSVa0FtDz0jHs="]
// } as Group}))
}, [])
useEffect(() => {
setDataTable(transformDataTable(peers))
}, [peers])
useEffect(() => {
setDataTable(transformDataTable(filterDataTable()))
}, [textToSearch, optionOnOff])
const deleteKey = 'deleting';
useEffect(() => {
const style = { marginTop: 85 }
if (deletedPeer.loading) {
message.loading({ content: 'Deleting...', key: deleteKey, style });
} else if (deletedPeer.success) {
message.success({ content: 'Peer deleted with success!', key: deleteKey, duration: 2, style });
} else if (deletedPeer.error) {
message.error({ content: 'Error! Something wrong to delete peer.', key: deleteKey, duration: 2, style });
}
}, [deletedPeer])
const filterDataTable = ():Peer[] => {
const t = textToSearch.toLowerCase().trim()
let f:Peer[] = filter(peers, (f:Peer) =>
(f.Name.toLowerCase().includes(t) || f.IP.includes(t) || f.OS.includes(t) || t === "")
) as Peer[]
if (optionOnOff === "on") {
f = filter(peers, (f:Peer) => f.Connected)
}
return f
}
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setTextToSearch(e.target.value)
};
const searchDataTable = () => {
const data = filterDataTable()
setDataTable(transformDataTable(data))
}
const onChangeOnOff = ({ target: { value } }: RadioChangeEvent) => {
setOptionOnOff(value)
}
const onChangePageSize = (value: string) => {
setPageSize(parseInt(value.toString()))
}
const showConfirmDelete = () => {
confirm({
icon: <ExclamationCircleOutlined />,
width: 600,
content: <Space direction="vertical" size="small">
{peerToAction &&
<>
<Title level={5}>Delete peer "{peerToAction ? peerToAction.Name : ''}"</Title>
<Paragraph>Are you sure you want to delete peer from your account?</Paragraph>
</>
}
</Space>,
okType: 'danger',
onOk() {
dispatch(peerActions.deletedPeer.request({getAccessTokenSilently, payload: peerToAction ? peerToAction.IP : ''}));
},
onCancel() {
setPeerToAction(null);
},
});
}
return (
<Container style={{paddingTop: "40px"}}>
<Row>
<Col span={24}>
<Title level={4}>Peers</Title>
<Paragraph>A list of all the machines in your account including their name, IP and status.</Paragraph>
<Space direction="vertical" size="large" style={{ display: 'flex' }}>
<Row gutter={[16, 24]}>
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
{/*<Input.Search allowClear value={textToSearch} onPressEnter={searchDataTable} onSearch={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />*/}
<Input allowClear value={textToSearch} onPressEnter={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />
</Col>
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
<Space size="middle">
<Radio.Group
options={optionsOnOff}
onChange={onChangeOnOff}
value={optionOnOff}
optionType="button"
buttonStyle="solid"
/>
<Select value={pageSize.toString()} options={pageSizeOptions} onChange={onChangePageSize} className="select-rows-per-page-en"/>
</Space>
</Col>
<Col xs={24}
sm={24}
md={5}
lg={5}
xl={5}
xxl={5} span={5}>
<Row justify="end">
<Col>
<Link to="/add-peer" className="ant-btn ant-btn-primary ant-btn-block">Add Peer</Link>
</Col>
</Row>
</Col>
</Row>
{failed &&
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
}
{loading && <Loading/>}
<Card bodyStyle={{padding: 0}}>
<Table
pagination={{pageSize, showSizeChanger: false, showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} peers`)}}
className="card-table"
showSorterTooltip={false}
scroll={{x: true}}
dataSource={dataTable}>
<Column title="Name" dataIndex="Name" key="Name"
onFilter={(value: string | number | boolean, record) => (record as any).Name.includes(value)}
sorter={(a, b) => ((a as any).Name.localeCompare((b as any).Name))} />
<Column title="IP" dataIndex="IP"
sorter={(a, b) => {
const _a = (a as any).IP.split('.')
const _b = (b as any).IP.split('.')
const a_s = _a.map((i:any) => i.padStart(3, '0')).join()
const b_s = _b.map((i:any) => i.padStart(3, '0')).join()
return a_s.localeCompare(b_s)
}}
render={(text, record, index) => {
return <ButtonCopyMessage keyMessage={(record as PeerDataTable).key} text={text} messageText={'IP copied!'} styleNotification={{}}/>
}}
/>
<Column title="Status" dataIndex="Connected"
render={(text, record, index) => {
return text ? <Tag color="green">online</Tag> : <Tag color="red">offline</Tag>
}}
/>
<Column title="LastSeen" dataIndex="LastSeen"
render={(text, record, index) => {
return (record as PeerDataTable).Connected ? 'just now' : timeAgo(text)
}}
/>
<Column title="OS" dataIndex="OS"
render={(text, record, index) => {
return formatOS(text)
}}
/>
<Column title="Version" dataIndex="Version" />
<Column title="" align="center"
render={(text, record, index) => {
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
onVisibleChange={visible => {
if (visible) setPeerToAction(record as PeerDataTable)
}}></Dropdown.Button>
}}
/>
</Table>
</Card>
</Space>
</Col>
</Row>
</Container>
)
}
export default withAuthenticationRequired(Peers,
{
onRedirecting: () => <Loading padding="3em" width="50px" height="50px"/>,
}
);

View File

@@ -1,310 +0,0 @@
import React, {useEffect, useState} from "react";
import {useAuth0, withAuthenticationRequired} from "@auth0/auth0-react";
import Loading from "../components/Loading";
import {formatDate, timeAgo} from "../utils/common";
import {createSetupKey, getSetupKeys, revokeSetupKey} from "../api/ManagementAPI";
import EditButton from "../components/EditButton";
import CopyText from "../components/CopyText";
import DeleteModal from "../components/DeleteDialog";
import NewSetupKeyDialog from "../components/NewSetupKeyDialog";
export const SetupKeysComponent = () => {
const [setupKeys, setSetupKeys] = useState([])
const [setupKeysBackUp, setSetupKeysBackUp] = useState([]);
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false)
const [deleteDialogText, setDeleteDialogText] = useState("")
const [deleteDialogTitle, setDeleteDialogTitle] = useState("")
const [keyToRevoke, setKeyToRevoke] = useState(null)
const handleSearch = (e) => {
let tempArray = setupKeysBackUp.filter((item) => {
return item.Name.toUpperCase().includes(e.toUpperCase())
});
setSetupKeys(tempArray);
};
const showValid = () => {
const showValidBtn = document.getElementById("btn-show-valid");
const showAllBtn = document.getElementById("btn-show-all");
showValidBtn.classList.add(
"ring-1",
"ring-indigo-500",
"border-indigo-500",
"outline-none"
);
showAllBtn.classList.remove(
"ring-1",
"ring-indigo-500",
"border-indigo-500",
"outline-none"
);
refresh(validFilter);
};
const validFilter = function (keys) {
return keys.filter((key) => {
return key.Valid;
});
}
const showAll = () => {
const showAllBtn = document.getElementById("btn-show-all");
const showValidBtn = document.getElementById("btn-show-valid");
showAllBtn.classList.add(
"ring-1",
"ring-indigo-500",
"border-indigo-500",
"outline-none"
);
showValidBtn.classList.remove(
"ring-1",
"ring-indigo-500",
"border-indigo-500",
"outline-none"
);
refresh(null)
};
const handleNewKeyClick = () => {
setShowNewKeyDialog(true)
}
const newSetupKeyDialogCallback = (cancelled, name, type, expiresIn) => {
if (!cancelled) {
createSetupKey(getAccessTokenSilently, name, type, expiresIn)
.then(() => refresh(validFilter))
.catch(error => {
console.log(error)
})
}
setShowNewKeyDialog(false)
}
const {
getAccessTokenSilently,
} = useAuth0();
const handleError = error => {
console.error('Error to fetch data:', error);
setLoading(false)
setError(error);
};
//called when user clicks on table row menu item
const handleRowMenuClick = (action, key) => {
if (action === 'Revoke') {
setKeyToRevoke(key)
setDeleteDialogText("Are you sure you want to revoke setup key?")
setDeleteDialogTitle("Revoke key \"" + key.Name + "\"")
setShowDeleteDialog(true)
}
};
// after user confirms (or not) revoking the key
const handleRevokeConfirmation = (confirmed) => {
setShowDeleteDialog(false)
if (confirmed && !keyToRevoke.Revoked) {
revokeSetupKey(getAccessTokenSilently, keyToRevoke.Id)
.then(() => setKeyToRevoke(null))
.then(() => refresh(validFilter))
.catch(error => {
setKeyToRevoke(null)
console.log(error)
})
} else {
setKeyToRevoke(null)
}
}
const refresh = (filter) => {
getSetupKeys(getAccessTokenSilently)
.then(responseData => responseData.sort((a, b) => (a.Name > b.Name) ? 1 : -1))
.then((sorted) => {
return filter != null ? filter(sorted) : sorted;
})
.then((filtered) => {
setSetupKeysBackUp(filtered);
setSetupKeys(filtered);
})
.then(() => setLoading(false))
.catch(error => handleError(error))
}
useEffect(() => {
refresh(validFilter)
}, [getAccessTokenSilently])
return (
<>
<div className="py-10 bg-gray-50 overflow-hidden rounded max-w-7xl mx-auto sm:px-6 lg:px-8">
<header className="sm:flex sm:items-center">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 sm:flex-auto">
<h1 className="text-xl font-semibold text-gray-900">Setup Keys</h1>
<p className="mt-2 text-sm text-gray-700">
A list of all the setup keys in your account including their name, state, type and
expiration.
</p>
</div>
</header>
<main>
{loading && (<Loading/>)}
{error != null && (
<span>{error.toString()}</span>
)}
{setupKeys && (
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="auto py-8">
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-6 gap-5">
<div className="lg:col-span-2">
<input
className="text-sm w-full rounded p-2 border border-gray-300 focus:border-gray-400 outline-none"
placeholder="Search..."
type="search"
onChange={(e) => handleSearch(e.target.value)}
/>
</div>
<div className="lg:col-span-2"/>
<div className="flex lg:justify-end justify-center">
<div className="flex items-center">
<span className="relative z-0 inline-flex shadow-sm rounded-md">
<button
id="btn-show-valid"
onClick={() => showValid()}
type="button"
className="relative inline-flex items-center px-4 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 z-10 outline-none ring-1 ring-indigo-500 border-indigo-500"
>
Valid
</button>
<button
type="button"
id="btn-show-all"
onClick={() => showAll()}
className="relative inline-flex items-center px-4 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-700 outline-none hover:bg-gray-50"
>
All
</button>
</span>
</div>
</div>
<div className="lg:flex lg:justify-end">
<button
type="button"
className="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto"
onClick={() => {
handleNewKeyClick()
}}
>
Add Key
</button>
</div>
</div>
</div>
<div className="px-4 py-2 sm:px-0">
<DeleteModal show={showDeleteDialog}
confirmCallback={handleRevokeConfirmation}
text={deleteDialogText} title={deleteDialogTitle}/>
<NewSetupKeyDialog show={showNewKeyDialog} closeCallback={newSetupKeyDialogCallback}/>
<div className="flex flex-col">
<div className="-my-2 sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div
className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{[
"Name",
"State",
"Type",
"Key",
"Last Used",
"Used Times",
"Expires",
].map((col) => {
return (
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
key={col}
>
{col}
</th>
);
})}
<th
scope="col"
className="relative px-6 py-3"
>
<span className="sr-only">
Edit
</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{setupKeys.map((setupKey, idx) => (
<tr key={setupKey.Id}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">{setupKey.Name}</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{setupKey.Valid && (
<span
className="inline-flex rounded-full bg-green-100 px-2 text-xs leading-5 text-green-800">
valid
</span>
)}
{!setupKey.Valid && (
<span
className="inline-flex rounded-full bg-red-100 px-2 text-xs leading-5 text-red-800">
{setupKey.State}
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{setupKey.Type.toLowerCase()}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">
<CopyText text={setupKey.Key.toUpperCase()}
idPrefix={"setup-keys" + setupKey.Id}/>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{setupKey.UsedTimes === 0 ? "unused" : timeAgo(setupKey.LastUsed)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{setupKey.UsedTimes}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{formatDate(setupKey.Expires)}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-m">
<EditButton items={[{name: "Revoke"}]}
handler={action => handleRowMenuClick(action, setupKey)}/>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</main>
</div>
</>
);
}
;
export default withAuthenticationRequired(SetupKeysComponent,
{
onRedirecting: () => <Loading/>,
}
);

328
src/views/SetupKeys.tsx Normal file
View File

@@ -0,0 +1,328 @@
import React, {useEffect, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {useAuth0, withAuthenticationRequired} from "@auth0/auth0-react";
import { RootState } from "typesafe-actions";
import { actions as setupKeyActions } from '../store/setup-key';
import Loading from "../components/Loading";
import {Container} from "../components/Container";
import {
Col,
Row,
Typography,
Table,
Card,
Tag,
Input,
Space,
Radio,
RadioChangeEvent,
Dropdown,
Menu,
Alert, Select, Modal, Button, message, Drawer, Form, List
} from "antd";
import {SetupKey, SetupKeyRevoke} from "../store/setup-key/types";
import {filter, transform} from "lodash"
import {copyToClipboard, formatDate, formatOS, timeAgo} from "../utils/common";
import {ExclamationCircleOutlined} from "@ant-design/icons";
import SetupKeyNew from "../components/SetupKeyNew";
import ButtonCopyMessage from "../components/ButtonCopyMessage";
const { Title, Text, Paragraph } = Typography;
const { Column } = Table;
const { confirm } = Modal;
interface SetupKeyDataTable extends SetupKey {
key: string
}
export const SetupKeys = () => {
const { getAccessTokenSilently } = useAuth0()
const dispatch = useDispatch()
const setupKeys = useSelector((state: RootState) => state.setupKey.data);
const failed = useSelector((state: RootState) => state.setupKey.failed);
const loading = useSelector((state: RootState) => state.setupKey.loading);
const deletedSetupKey = useSelector((state: RootState) => state.setupKey.deletedSetupKey);
const revokedSetupKey = useSelector((state: RootState) => state.setupKey.revokedSetupKey);
const createdSetupKey = useSelector((state: RootState) => state.setupKey.createdSetupKey);
const [textToSearch, setTextToSearch] = useState('');
const [optionValidAll, setOptionValidAll] = useState('valid');
const [pageSize, setPageSize] = useState(5);
const [dataTable, setDataTable] = useState([] as SetupKeyDataTable[]);
const [setupKeyToAction, setSetupKeyToAction] = useState(null as SetupKeyDataTable | null);
const styleNotification = { marginTop: 85 }
const pageSizeOptions = [
{label: "5", value: "5"},
{label: "10", value: "10"},
{label: "15", value: "15"}
]
const optionsValidAll = [{label: 'Valid', value: 'valid'}, {label: 'All', value: 'all'}]
const itemsMenuAction = [
{
key: "revoke",
label: (<Button type="text" onClick={() => showConfirmRevoke()}>Revoke</Button>)
},
/*{
key: "delete",
label: (<Button type="text" onClick={() => showConfirmDelete()}>Delete</Button>)
}*/
]
const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>)
const transformDataTable = (d:SetupKey[]):SetupKeyDataTable[] => {
return d.map(p => ({ key: p.Id, ...p } as SetupKeyDataTable))
}
useEffect(() => {
dispatch(setupKeyActions.getSetupKeys.request({getAccessTokenSilently, payload: null}));
}, [])
useEffect(() => {
setDataTable(transformDataTable(filterDataTable()))
}, [setupKeys])
useEffect(() => {
setDataTable(transformDataTable(filterDataTable()))
}, [textToSearch, optionValidAll])
const deleteKey = 'deleting';
useEffect(() => {
if (deletedSetupKey.loading) {
message.loading({ content: 'Deleting...', key: deleteKey, style: styleNotification });
} else if (deletedSetupKey.success) {
message.success({ content: 'SetupKey deleted with success!', key: deleteKey, duration: 2, style: styleNotification });
dispatch(setupKeyActions.setDeleteSetupKey({ ...deletedSetupKey, success: false }));
} else if (deletedSetupKey.error) {
message.error({ content: 'Error! Something wrong to delete setupKey.', key: deleteKey, duration: 2, style: styleNotification });
dispatch(setupKeyActions.setDeleteSetupKey({ ...deletedSetupKey, error: null }));
}
}, [deletedSetupKey])
const revokeKey = 'creating';
useEffect(() => {
if (revokedSetupKey.loading) {
message.loading({ content: 'Creating...', key: revokeKey, duration: 0, style: styleNotification });
} else if (revokedSetupKey.success) {
message.success({ content: 'Key was revoked with success!', key: revokeKey, duration: 2, style: styleNotification });
dispatch(setupKeyActions.setRevokeSetupKey({ ...revokedSetupKey, success: false }));
} else if (revokedSetupKey.error) {
message.error({ content: 'Error! Something wrong to revoke key.', key: revokeKey, duration: 2, style: styleNotification });
dispatch(setupKeyActions.setRevokeSetupKey({ ...revokedSetupKey, error: null }));
}
}, [revokedSetupKey])
const createKey = 'creating';
useEffect(() => {
if (createdSetupKey.loading) {
message.loading({ content: 'Creating...', key: createKey, duration: 0, style: styleNotification });
} else if (createdSetupKey.success) {
message.success({ content: 'Key created with success!', key: createKey, duration: 2, style: styleNotification });
dispatch(setupKeyActions.setSetupNewKeyVisible(false));
dispatch(setupKeyActions.setCreateSetupKey({ ...createdSetupKey, success: false }));
} else if (createdSetupKey.error) {
message.error({ content: 'Error! Something wrong to create key.', key: createKey, duration: 2, style: styleNotification });
dispatch(setupKeyActions.setCreateSetupKey({ ...createdSetupKey, error: null }));
}
}, [createdSetupKey])
const filterDataTable = ():SetupKey[] => {
const t = textToSearch.toLowerCase().trim()
let f:SetupKey[] = [...setupKeys]
if (optionValidAll === "valid") {
f = filter(setupKeys, (_f:SetupKey) => _f.Valid && !_f.Revoked)
}
f = filter(f, (_f:SetupKey) =>
(_f.Name.toLowerCase().includes(t) || _f.State.includes(t) || _f.Type.includes(t) || _f.Key.includes(t) || t === "")
) as SetupKey[]
return f
}
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setTextToSearch(e.target.value)
};
const searchDataTable = () => {
const data = filterDataTable()
setDataTable(transformDataTable(data))
}
const onChangeValidAll = ({ target: { value } }: RadioChangeEvent) => {
setOptionValidAll(value)
}
const onChangePageSize = (value: string) => {
setPageSize(parseInt(value.toString()))
}
const showConfirmDelete = () => {
confirm({
icon: <ExclamationCircleOutlined />,
width: 600,
content: <Space direction="vertical" size="small">
{setupKeyToAction &&
<>
<Title level={5}>Delete setupKey "{setupKeyToAction ? setupKeyToAction.Name : ''}"</Title>
<Paragraph>Are you sure you want to delete key?</Paragraph>
</>
}
</Space>,
okType: 'danger',
onOk() {
dispatch(setupKeyActions.deleteSetupKey.request({getAccessTokenSilently, payload: setupKeyToAction ? setupKeyToAction.Id : ''}));
},
onCancel() {
setSetupKeyToAction(null);
},
});
}
const showConfirmRevoke = () => {
confirm({
icon: <ExclamationCircleOutlined />,
width: 600,
content: <Space direction="vertical" size="small">
{setupKeyToAction &&
<>
<Title level={5}>Revoke setupKey "{setupKeyToAction ? setupKeyToAction.Name : ''}"</Title>
<Paragraph>Are you sure you want to revoke key?</Paragraph>
</>
}
</Space>,
okType: 'danger',
onOk() {
dispatch(setupKeyActions.revokeSetupKey.request({getAccessTokenSilently, payload: { Id: setupKeyToAction ? setupKeyToAction.Id : null, Revoked: true } as SetupKeyRevoke}));
},
onCancel() {
setSetupKeyToAction(null);
},
});
}
const onClickAddNewSetupKey = () => {
dispatch(setupKeyActions.setSetupNewKeyVisible(true));
dispatch(setupKeyActions.setSetupKey({
Name: '',
Type: 'reusable'
} as SetupKey))
}
return (
<>
<Container style={{paddingTop: "40px"}}>
<Row>
<Col span={24}>
<Title level={4}>Setup Keys</Title>
<Paragraph>A list of all the setup keys in your account including their name, state, type and expiration.</Paragraph>
<Space direction="vertical" size="large" style={{ display: 'flex' }}>
<Row gutter={[16, 24]}>
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
{/*<Input.Search allowClear value={textToSearch} onPressEnter={searchDataTable} onSearch={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />*/}
<Input allowClear value={textToSearch} onPressEnter={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />
</Col>
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
<Space size="middle">
<Radio.Group
options={optionsValidAll}
onChange={onChangeValidAll}
value={optionValidAll}
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>
<Button type="primary" onClick={onClickAddNewSetupKey}>Add Key</Button>
</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} setup keys`)}}
className="card-table"
showSorterTooltip={false}
scroll={{x: true}}
dataSource={dataTable}>
<Column title="Name" dataIndex="Name"
onFilter={(value: string | number | boolean, record) => (record as any).Name.includes(value)}
sorter={(a, b) => ((a as any).Name.localeCompare((b as any).Name))}
/>
<Column title="State" dataIndex="State"
render={(text, record, index) => {
return (text === 'valid') ? <Tag color="green">{text}</Tag> : <Tag color="red">{text}</Tag>
}}
sorter={(a, b) => ((a as any).State.localeCompare((b as any).State))}
/>
<Column title="Type" dataIndex="Type"
onFilter={(value: string | number | boolean, record) => (record as any).Type.includes(value)}
sorter={(a, b) => ((a as any).Type.localeCompare((b as any).Type))}
/>
<Column title="Key" dataIndex="Key"
onFilter={(value: string | number | boolean, record) => (record as any).Key.includes(value)}
sorter={(a, b) => ((a as any).Key.localeCompare((b as any).Key))}
render={(text, record, index) => {
return <ButtonCopyMessage keyMessage={(record as SetupKeyDataTable).key} text={text} messageText={`Key copied!`} styleNotification={{}}/>
}}
/>
<Column title="Last Used" dataIndex="LastUsed"
render={(text, record, index) => {
return !(record as SetupKey).UsedTimes ? 'unused' : timeAgo(text)
}}
/>
<Column title="Used Times" dataIndex="UsedTimes"
sorter={(a, b) => ((a as any).Type.localeCompare((b as any).Type))}
/>
<Column title="Expires" dataIndex="Expires"
render={(text, record, index) => {
return formatDate(text)
}}
/>
<Column title="" align="center"
render={(text, record, index) => {
return !(record as SetupKeyDataTable).Revoked ? (
<Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
onVisibleChange={visible => {
if (visible) setSetupKeyToAction(record as SetupKeyDataTable)
}}></Dropdown.Button>) : <></>
}}
/>
</Table>
</Card>
</Space>
</Col>
</Row>
</Container>
<SetupKeyNew/>
</>
)
}
export default withAuthenticationRequired(SetupKeys,
{
onRedirecting: () => <Loading padding="3em" width="50px" height="50px"/>,
}
);

Some files were not shown because too many files have changed in this diff Show More