Compare commits

...
4 Commits
Author SHA1 Message Date
Sarooj Bukhari 8ad6952dae da 2023-08-25 14:44:10 +05:00
Sarooj Bukhari 7e5595c365 fix button hover 2023-08-15 21:39:36 +05:00
Sarooj Bukhari 12687379cc limit user account 2023-08-15 21:04:52 +05:00
braginini 62d80af186 Limit columns and navbar 2023-08-15 15:37:37 +02:00
9 changed files with 394 additions and 207 deletions

No files matched your search

+33
View File
@@ -12,6 +12,8 @@
"@axa-fr/react-oidc": "^5.14.0",
"@headlessui/react": "^1.5.0",
"@heroicons/react": "^1.0.4",
"@stripe/react-stripe-js": "^2.1.2",
"@stripe/stripe-js": "^2.1.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
@@ -3405,6 +3407,24 @@
"@sinonjs/commons": "^1.7.0"
}
},
"node_modules/@stripe/react-stripe-js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.1.2.tgz",
"integrity": "sha512-2JcKRvOdMD698uGrY0cINLHb6XHnY24L+wlWlw2+TbI2aXlQZ9t8Mc+KssFES+EuNgISbrp7fJR3w/13epfiOw==",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"@stripe/stripe-js": "^1.44.1 || ^2.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@stripe/stripe-js": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-2.1.0.tgz",
"integrity": "sha512-AdMHWqIq/s3TLmoyuzdb6l21STKDKNm82cjce1VelyQGinVNjVgSEPH5UDtxENEP7OzgMZH8RTphFMi0fMJGLQ=="
},
"node_modules/@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
@@ -19477,6 +19497,19 @@
"@sinonjs/commons": "^1.7.0"
}
},
"@stripe/react-stripe-js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.1.2.tgz",
"integrity": "sha512-2JcKRvOdMD698uGrY0cINLHb6XHnY24L+wlWlw2+TbI2aXlQZ9t8Mc+KssFES+EuNgISbrp7fJR3w/13epfiOw==",
"requires": {
"prop-types": "^15.7.2"
}
},
"@stripe/stripe-js": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-2.1.0.tgz",
"integrity": "sha512-AdMHWqIq/s3TLmoyuzdb6l21STKDKNm82cjce1VelyQGinVNjVgSEPH5UDtxENEP7OzgMZH8RTphFMi0fMJGLQ=="
},
"@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
+4 -2
View File
@@ -7,6 +7,8 @@
"@axa-fr/react-oidc": "^5.14.0",
"@headlessui/react": "^1.5.0",
"@heroicons/react": "^1.0.4",
"@stripe/react-stripe-js": "^2.1.2",
"@stripe/stripe-js": "^2.1.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
@@ -79,7 +81,7 @@
]
},
"devDependencies": {
"@types/react-syntax-highlighter": "^15.5.3",
"@playwright/test": "^1.36.2"
"@playwright/test": "^1.36.2",
"@types/react-syntax-highlighter": "^15.5.3"
}
}
View File
Whitespace-only changes.
+1
View File
@@ -53,6 +53,7 @@ const Navbar = () => {
"/acls",
"/routes",
"/dns",
"/users",
"/activity",
"/settings",
];
+3 -2
View File
@@ -166,6 +166,7 @@ const UserEdit = (props: any) => {
dispatch(userActions.setUser(null as unknown as User));
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null));
dispatch(userActions.setUserTabOpen(key));
dispatch(userActions.setEditUserPopupVisible(false));
};
const selectValidator = (_: RuleObject, value: string[]) => {
@@ -331,7 +332,7 @@ const UserEdit = (props: any) => {
// menu: { items: menuItems },
},
{
title: user.name,
title: user?.name,
},
]}
/>
@@ -386,7 +387,7 @@ const UserEdit = (props: any) => {
style={{ marginRight: "70px", fontWeight: "500" }}
>
<Input
disabled={user.id}
disabled={user?.id}
value={formUser.email}
style={{ color: "#8c8c8c" }}
autoComplete="off"
+4
View File
@@ -515,4 +515,8 @@ ul.ant-list-items {
.d-none{
display: none!important;
}
.nohover {
background: transparent!important;
cursor: text;
}
+204 -135
View File
@@ -1,198 +1,268 @@
import {all, call, spawn, put, select, takeLatest} from 'redux-saga/effects';
import { all, call, spawn, put, select, takeLatest } from "redux-saga/effects";
import {
ApiError,
ApiResponse,
ChangeResponse,
CreateResponse,
DeleteResponse
} from '../../services/api-client/types';
import { Peer } from './types'
import service from './service';
import actions from './actions';
import {Group, GroupPeer} from "../group/types";
DeleteResponse,
} from "../../services/api-client/types";
import { Peer } from "./types";
import service from "./service";
import actions from "./actions";
import { Group, GroupPeer } from "../group/types";
import serviceGroup from "../group/service";
import {actions as groupActions} from "../group";
export function* getPeers(action: ReturnType<typeof actions.getPeers.request>): Generator {
import { actions as groupActions } from "../group";
import userService from "../user/service";
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>)
);
yield put(actions.setDeletePeer({
loading: false,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>))
const users: any = yield call(userService.getUsers, action.payload);
let currentUser = users.body.find((user: any) => user.is_current);
const effect = yield call(service.getPeers, action.payload);
const response = effect as ApiResponse<Peer[]>;
yield put(actions.getPeers.success(response.body));
let peersBody: any;
if (currentUser.role === "user") {
peersBody = response.body.filter((pe) => {
return pe.user_id === currentUser.id;
});
} else {
peersBody = response.body;
}
yield put(actions.getPeers.success(peersBody));
} 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* setDeletePeer(
action: ReturnType<typeof actions.setDeletePeer>
): Generator {
yield put(actions.setDeletePeer(action.payload));
}
export function* deletePeer(action: ReturnType<typeof actions.deletedPeer.request>): Generator {
export function* deletePeer(
action: ReturnType<typeof actions.deletedPeer.request>
): Generator {
try {
yield call(actions.setDeletePeer,{
yield call(actions.setDeletePeer, {
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | 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>));
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.id !== action.payload.payload)))
const peers = (yield select((state) => state.peer.data)) as Peer[];
yield put(
actions.getPeers.success(
peers.filter((p: Peer) => p.id !== 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>));
yield put(
actions.deletedPeer.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null,
} as DeleteResponse<string | null>)
);
}
}
export function* saveGroups(action: ReturnType<typeof actions.saveGroups.request>): Generator {
export function* saveGroups(
action: ReturnType<typeof actions.saveGroups.request>
): Generator {
try {
yield put(actions.setSavedGroups({
loading: true,
success: false,
failure: false,
error: null,
data: null
}))
yield put(
actions.setSavedGroups({
loading: true,
success: false,
failure: false,
error: null,
data: null,
})
);
const currentGroups = [...(yield select(state => state.group.data)) as Group[]]
const currentGroups = [
...((yield select((state) => state.group.data)) as Group[]),
];
const peerGroupsToSave = action.payload.payload
const peerGroupsToSave = action.payload.payload;
let groupsToSave = [] as Group[]
let groupsNoId = [] as Group[]
let groupsToSave = [] as Group[];
let groupsNoId = [] as Group[];
groupsToSave = groupsToSave.concat(
currentGroups
.filter(g => peerGroupsToSave.groupsToRemove.includes(g.id || ''))
.map(g => ({
id: g.id,
name: g.name,
peers: (g.peers as GroupPeer[]).filter(p => p.id !== peerGroupsToSave.ID).map(p => p.id) as string[]
}))
)
currentGroups
.filter((g) => peerGroupsToSave.groupsToRemove.includes(g.id || ""))
.map((g) => ({
id: g.id,
name: g.name,
peers: (g.peers as GroupPeer[])
.filter((p) => p.id !== peerGroupsToSave.ID)
.map((p) => p.id) as string[],
}))
);
groupsToSave = groupsToSave.concat(
currentGroups
.filter(g => peerGroupsToSave.groupsToAdd.includes(g.id || ''))
.map(g => ({
id: g.id,
name: g.name,
Peers: g.peers ? [...(g.peers as GroupPeer[]).map((p:GroupPeer) => p.id), peerGroupsToSave.ID] : [peerGroupsToSave.ID]
}))
)
currentGroups
.filter((g) => peerGroupsToSave.groupsToAdd.includes(g.id || ""))
.map((g) => ({
id: g.id,
name: g.name,
Peers: g.peers
? [
...(g.peers as GroupPeer[]).map((p: GroupPeer) => p.id),
peerGroupsToSave.ID,
]
: [peerGroupsToSave.ID],
}))
);
groupsNoId = peerGroupsToSave.groupsNoId.map(g => ({
groupsNoId = peerGroupsToSave.groupsNoId.map((g) => ({
name: g,
peers: [peerGroupsToSave.ID]
}))
peers: [peerGroupsToSave.ID],
}));
if (!groupsNoId.length && !groupsToSave.length) {
return
return;
}
const responsesGroup = yield all(groupsToSave.map(g => call(serviceGroup.editGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: g
})
))
const responsesGroup = yield all(
groupsToSave.map((g) =>
call(serviceGroup.editGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: g,
})
)
);
const responsesGroupNoId = yield all(groupsNoId.map(g => call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: g
})
))
yield put(actions.saveGroups.success({
loading: false,
success: true,
failure: false,
error: null,
data: [...(responsesGroup as ApiResponse<Group>[]).map(r => r.body), ...(responsesGroupNoId as ApiResponse<Group>[]).map(r => r.body)]
} as CreateResponse<Group[] | null>))
const responsesGroupNoId = yield all(
groupsNoId.map((g) =>
call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: g,
})
)
);
yield put(groupActions.getGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
yield put(actions.getPeers.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
yield put(
actions.saveGroups.success({
loading: false,
success: true,
failure: false,
error: null,
data: [
...(responsesGroup as ApiResponse<Group>[]).map((r) => r.body),
...(responsesGroupNoId as ApiResponse<Group>[]).map((r) => r.body),
],
} as CreateResponse<Group[] | null>)
);
yield put(
groupActions.getGroups.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null,
})
);
yield put(
actions.getPeers.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null,
})
);
} catch (err) {
console.log(err)
yield put(actions.saveGroups.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as ChangeResponse<Group[] | null>));
console.log(err);
yield put(
actions.saveGroups.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null,
} as ChangeResponse<Group[] | null>)
);
}
}
export function* updatePeer(action: ReturnType<typeof actions.updatePeer.request>): Generator {
export function* updatePeer(
action: ReturnType<typeof actions.updatePeer.request>
): Generator {
try {
yield put(actions.setUpdatedPeer({
loading: true,
success: false,
failure: false,
error: null,
data: null
}))
yield put(
actions.setUpdatedPeer({
loading: true,
success: false,
failure: false,
error: null,
data: null,
})
);
const peer = action.payload.payload
const peerId = peer.id
const peer = action.payload.payload;
const peerId = peer.id;
const payloadToSave = {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: peer
}
payload: peer,
};
const effect = yield call(service.updatePeer, payloadToSave)
const effect = yield call(service.updatePeer, payloadToSave);
const response = effect as ApiResponse<Peer>;
yield put(actions.updatePeer.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as ChangeResponse<Peer | null>));
const peers = (yield select(state => state.peer.data)) as Peer[]
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.id !== peerId).concat(response.body)))
yield put(
actions.updatePeer.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as ChangeResponse<Peer | null>)
);
const peers = (yield select((state) => state.peer.data)) as Peer[];
yield put(
actions.getPeers.success(
peers.filter((p: Peer) => p.id !== peerId).concat(response.body)
)
);
} catch (err) {
console.log(err)
yield put(actions.updatePeer.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as ChangeResponse<Peer | null>));
console.log(err);
yield put(
actions.updatePeer.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null,
} as ChangeResponse<Peer | null>)
);
}
}
@@ -201,7 +271,6 @@ export default function* sagas(): Generator {
takeLatest(actions.getPeers.request, getPeers),
takeLatest(actions.deletedPeer.request, deletePeer),
takeLatest(actions.saveGroups.request, saveGroups),
takeLatest(actions.updatePeer.request, updatePeer)
takeLatest(actions.updatePeer.request, updatePeer),
]);
}
+76 -67
View File
@@ -434,6 +434,7 @@ export const Peers = () => {
};
const setUpdateGroupsVisible = (peerToAction: Peer, status: boolean) => {
if (!isAdmin) return;
if (status) {
dispatch(peerActions.setPeer({ ...peerToAction }));
dispatch(peerActions.setUpdateGroupsVisible(true));
@@ -502,10 +503,7 @@ export const Peers = () => {
open={groupPopupVisible === peerToAction.key}
title={null}
>
<Button
type="link"
onClick={() => setGroupVisible(peerToAction, true)}
>
<Button type="link" onClick={() => setGroupVisible(peerToAction, true)}>
{label}
</Button>
</Popover>
@@ -592,6 +590,7 @@ export const Peers = () => {
type="text"
style={{ height: "auto", whiteSpace: "normal", textAlign: "left" }}
onClick={() => setUpdateGroupsVisible(peer, true)}
className={!isAdmin ? "nohover" : ""}
>
<span style={{ textAlign: "left" }}>
<Row>
@@ -607,6 +606,7 @@ export const Peers = () => {
<div>
<Button
type="text"
className={!isAdmin ? "nohover" : ""}
style={{ height: "auto", textAlign: "left" }}
onClick={() => setUpdateGroupsVisible(peer, true)}
>
@@ -690,22 +690,28 @@ export const Peers = () => {
/>
</Space>
<Select
mode="tags"
placeholder="Filter by groups"
tagRender={blueTagRender}
// dropdownRender={dropDownRender}
optionFilterProp="serchValue"
className="groupsSelect"
onChange={handleGroupChange}
value={groupsFilterArray}
>
{tagGroups.map((m, index) => (
<Option key={index} value={m.id} serchValue={m.name}>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
{isAdmin && (
<Select
mode="tags"
placeholder="Filter by groups"
tagRender={blueTagRender}
// dropdownRender={dropDownRender}
optionFilterProp="serchValue"
className="groupsSelect"
onChange={handleGroupChange}
value={groupsFilterArray}
>
{tagGroups.map((m, index) => (
<Option
key={index}
value={m.id}
serchValue={m.name}
>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
)}
</Col>
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
<Row justify="end">
@@ -757,7 +763,7 @@ export const Peers = () => {
Get started by adding one to your network.
</Paragraph>
<Button
data-testid="add-new-peer-button"
data-testid="add-new-peer-button"
size={"middle"}
type="primary"
onClick={() => setAddPeerModalOpen(true)}
@@ -822,53 +828,56 @@ export const Peers = () => {
return renderAddress(record);
}}
/>
<Column
title="Groups"
dataIndex="groupsCount"
align="center"
render={(text, record: PeerDataTable, index) => {
return renderPopoverGroups(
text,
record.groups,
record
);
}}
/>
<Column
title="SSH Server"
dataIndex="ssh_enabled"
align="center"
render={(e, record: PeerDataTable, index) => {
let isWindows = record.os
.toLocaleLowerCase()
.startsWith("windows");
let toggle = (
<Switch
size={"small"}
checked={e}
disabled={isWindows}
onClick={(checked: boolean) => {
if (checked) {
showConfirmEnableSSH(record);
} else {
handleSwitchSSH(record, checked);
}
}}
/>
);
if (isWindows) {
return (
<Tooltip title="SSH server feature is not yet supported on Windows">
{toggle}
</Tooltip>
);
} else {
return toggle;
}
}}
/>
{isAdmin && (
<>
<Column
title="Groups"
dataIndex="groupsCount"
align="center"
render={(text, record: PeerDataTable, index) => {
return renderPopoverGroups(
text,
record.groups,
record
);
}}
/>
<Column
title="SSH Server"
dataIndex="ssh_enabled"
align="center"
render={(e, record: PeerDataTable, index) => {
let isWindows = record.os
.toLocaleLowerCase()
.startsWith("windows");
let toggle = (
<Switch
size={"small"}
checked={e}
disabled={isWindows}
onClick={(checked: boolean) => {
if (checked) {
showConfirmEnableSSH(record);
} else {
handleSwitchSSH(record, checked);
}
}}
/>
);
if (isWindows) {
return (
<Tooltip title="SSH server feature is not yet supported on Windows">
{toggle}
</Tooltip>
);
} else {
return toggle;
}
}}
/>
</>
)}
<Column
title="LastSeen"
dataIndex="last_seen"
+69 -1
View File
@@ -33,6 +33,7 @@ import ExpiresInInput, {
expiresInToSeconds,
secondsToExpiresIn,
} from "./ExpiresInInput";
// import CheckoutPage from "../components/CheckoutForm";
import Column from "antd/lib/table/Column";
import TableSpin from "../components/Spin";
import { checkExpiresIn } from "../utils/common";
@@ -55,6 +56,8 @@ const { Title, Paragraph, Text } = Typography;
const styleNotification = { marginTop: 85 };
export const Settings = () => {
const [paymentCompleted, setPaymentCompleted] = useState(false);
const { getTokenSilently } = useGetTokenSilently();
const dispatch = useDispatch();
const { pageSize, onChangePageSize, pageSizeOptions } = usePageSizeHelpers(
@@ -571,7 +574,11 @@ export const Settings = () => {
"System settings",
"sub2",
<SettingOutlined />,
[getItem("Authentication", "auth"), getItem("Groups", "groups")],
[
getItem("Authentication", "auth"),
getItem("Groups", "groups"),
getItem("Billing", "billing"),
],
"group"
),
];
@@ -1122,6 +1129,67 @@ export const Settings = () => {
</Col>
</Row>
)}
{billingClicked && (
<Row>
<Col span={24}>
<Space
direction="vertical"
size="large"
style={{ display: "flex" }}
>
<Row style={{ width: "100%" }}>
<Col span={24}>
<Card loading={loading}>
<div
style={{
color: "rgba(0, 0, 0, 0.88)",
fontWeight: "500",
fontSize: "18px",
marginBottom: "20px",
}}
>
Billing - Free plan
</div>
<Row>
<Col span={12} style={{ marginBottom: "20px" }}>
<Alert
description="The Free plan allows you to add up to 5 users and 100 peers."
type="info"
action={
<Space>
<Button size="small" type="primary">
Upgrade
</Button>
</Space>
}
/>
</Col>
<Space size={30} wrap></Space>
<Col span={24}>
<Progress
strokeColor={"#1890FF"}
percent={60}
size={[200, 20]}
format={(percent) => `${users.length}/5 users`}
/>
</Col>
<Col span={24}>
<Progress
strokeColor={"#1890FF"}
percent={10}
size={[500, 20]}
format={(percent) => `10/100 peers`}
/>
</Col>
{/* <CheckoutPage /> */}
</Row>
</Card>
</Col>
</Row>
</Space>
</Col>
</Row>
)}
</Col>
</Row>
</Container>