mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
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:
10
README.md
10
README.md
@@ -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
17799
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
90
src/App.js
90
src/App.js
@@ -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;
|
||||
@@ -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
104
src/App.tsx
Normal 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;
|
||||
@@ -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)
|
||||
}
|
||||
3
src/assets/access_control_tutorial.svg
Normal file
3
src/assets/access_control_tutorial.svg
Normal file
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
3
src/assets/direct_bi.svg
Normal 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
3
src/assets/direct_in.svg
Normal 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 |
3
src/assets/direct_out.svg
Normal file
3
src/assets/direct_out.svg
Normal 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 |
42
src/components/AccessControlModalGroups.tsx
Normal file
42
src/components/AccessControlModalGroups.tsx
Normal 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;
|
||||
293
src/components/AccessControlNew.tsx
Normal file
293
src/components/AccessControlNew.tsx
Normal 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 <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 /> 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
|
||||
@@ -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">→</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
54
src/components/Banner.tsx
Normal 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 <span aria-hidden="true">→</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
|
||||
25
src/components/ButtonCopyMessage.tsx
Normal file
25
src/components/ButtonCopyMessage.tsx
Normal 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
|
||||
39
src/components/Container.tsx
Normal file
39
src/components/Container.tsx
Normal 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;
|
||||
}
|
||||
`
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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">
|
||||
​
|
||||
</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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
13
src/components/FooterComponent.tsx
Normal file
13
src/components/FooterComponent.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
17
src/components/Loading.tsx
Normal file
17
src/components/Loading.tsx
Normal 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;
|
||||
@@ -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
118
src/components/Navbar.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
141
src/components/SetupKeyNew.tsx
Normal file
141
src/components/SetupKeyNew.tsx
Normal 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
|
||||
@@ -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;
|
||||
@@ -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 = {};
|
||||
81
src/components/addpeer/LinuxTab.tsx
Normal file
81
src/components/addpeer/LinuxTab.tsx
Normal 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
|
||||
@@ -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 = {};
|
||||
55
src/components/addpeer/MacTab.tsx
Normal file
55
src/components/addpeer/MacTab.tsx
Normal 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
|
||||
@@ -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
|
||||
79
src/components/addpeer/TabSteps.tsx
Normal file
79
src/components/addpeer/TabSteps.tsx
Normal 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;
|
||||
@@ -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 = {};
|
||||
40
src/components/addpeer/WindowsTab.tsx
Normal file
40
src/components/addpeer/WindowsTab.tsx
Normal 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
|
||||
9
src/components/addpeer/types.ts
Normal file
9
src/components/addpeer/types.ts
Normal 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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
let configJson = "";
|
||||
let configJson:any = "";
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
configJson = require("./.local-config.json");
|
||||
123
src/index.css
123
src/index.css
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
68
src/services/api-client/api-client.ts
Normal file
68
src/services/api-client/api-client.ts
Normal 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 };
|
||||
45
src/services/api-client/api-request.ts
Normal file
45
src/services/api-client/api-request.ts
Normal 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 };
|
||||
23
src/services/api-client/header-factory.ts
Normal file
23
src/services/api-client/header-factory.ts
Normal 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 };
|
||||
10
src/services/api-client/index.ts
Normal file
10
src/services/api-client/index.ts
Normal 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 };
|
||||
63
src/services/api-client/types.ts
Normal file
63
src/services/api-client/types.ts
Normal 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;
|
||||
}
|
||||
29
src/services/local/index.ts
Normal file
29
src/services/local/index.ts
Normal 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
|
||||
}
|
||||
30
src/store/group/actions.ts
Normal file
30
src/store/group/actions.ts
Normal 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
7
src/store/group/index.ts
Normal 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 };
|
||||
89
src/store/group/reducer.ts
Normal file
89
src/store/group/reducer.ts
Normal 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
116
src/store/group/sagas.ts
Normal 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)
|
||||
]);
|
||||
}
|
||||
|
||||
37
src/store/group/service.ts
Normal file
37
src/store/group/service.ts
Normal 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
6
src/store/group/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Group {
|
||||
ID?: string;
|
||||
Name: string;
|
||||
Peers?: any[];
|
||||
PeersCount?: string;
|
||||
}
|
||||
27
src/store/index.ts
Normal file
27
src/store/index.ts
Normal 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
22
src/store/peer/actions.ts
Normal 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
7
src/store/peer/index.ts
Normal 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
66
src/store/peer/reducer.ts
Normal 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
71
src/store/peer/sagas.ts
Normal 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
18
src/store/peer/service.ts
Normal 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
9
src/store/peer/types.ts
Normal 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
13
src/store/root-action.ts
Normal 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
15
src/store/root-reducer.ts
Normal 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
31
src/store/rule/actions.ts
Normal 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
7
src/store/rule/index.ts
Normal 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
87
src/store/rule/reducer.ts
Normal 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
155
src/store/rule/sagas.ts
Normal 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
31
src/store/rule/service.ts
Normal 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
15
src/store/rule/types.ts
Normal 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[]
|
||||
}
|
||||
45
src/store/setup-key/actions.ts
Normal file
45
src/store/setup-key/actions.ts
Normal 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;
|
||||
7
src/store/setup-key/index.ts
Normal file
7
src/store/setup-key/index.ts
Normal 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 };
|
||||
102
src/store/setup-key/reducer.ts
Normal file
102
src/store/setup-key/reducer.ts
Normal 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
|
||||
});
|
||||
153
src/store/setup-key/sagas.ts
Normal file
153
src/store/setup-key/sagas.ts
Normal 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)
|
||||
]);
|
||||
}
|
||||
|
||||
36
src/store/setup-key/service.ts
Normal file
36
src/store/setup-key/service.ts
Normal 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
|
||||
);
|
||||
},
|
||||
};
|
||||
23
src/store/setup-key/types.ts
Normal file
23
src/store/setup-key/types.ts
Normal 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
12
src/store/types.d.ts
vendored
Normal 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
14
src/store/user/actions.ts
Normal 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
7
src/store/user/index.ts
Normal 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
38
src/store/user/reducer.ts
Normal 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
23
src/store/user/sagas.ts
Normal 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
12
src/store/user/service.ts
Normal 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
6
src/store/user/types.ts
Normal 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
4
src/types/images.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*.jpg';
|
||||
declare module '*.jpeg';
|
||||
declare module '*.png';
|
||||
declare module '*.svg';
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
396
src/views/AccessControl.tsx
Normal 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/>,
|
||||
}
|
||||
);
|
||||
@@ -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
36
src/views/Activity.tsx
Normal 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/>,
|
||||
}
|
||||
);
|
||||
@@ -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
62
src/views/AddPeer.tsx
Normal 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/>,
|
||||
}
|
||||
)
|
||||
@@ -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: </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
257
src/views/Peers.tsx
Normal 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"/>,
|
||||
}
|
||||
);
|
||||
@@ -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
328
src/views/SetupKeys.tsx
Normal 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
Reference in New Issue
Block a user