mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
Update setup key edit layout (#190)
https://github.com/netbirdio/dashboard/issues/187
This commit is contained in:
505
src/components/SetupKeyEdit.tsx
Normal file
505
src/components/SetupKeyEdit.tsx
Normal file
@@ -0,0 +1,505 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { actions as setupKeyActions } from "../store/setup-key";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Select,
|
||||
Breadcrumb,
|
||||
Switch,
|
||||
Tag,
|
||||
Typography,
|
||||
Card,
|
||||
} from "antd";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import {
|
||||
FormSetupKey,
|
||||
SetupKey,
|
||||
SetupKeyToSave,
|
||||
} from "../store/setup-key/types";
|
||||
import { formatDate, timeAgo } from "../utils/common";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import { CustomTagProps } from "rc-select/lib/BaseSelect";
|
||||
import { Group } from "../store/group/types";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { expiresInToSeconds, ExpiresInValue } from "../views/ExpiresInInput";
|
||||
import moment from "moment";
|
||||
import { Container } from "./Container";
|
||||
import Paragraph from "antd/es/typography/Paragraph";
|
||||
import { EditOutlined, LockOutlined } from "@ant-design/icons";
|
||||
import { actions as personalAccessTokenActions } from "../store/personal-access-token";
|
||||
|
||||
const { Option } = Select;
|
||||
const { Text } = Typography;
|
||||
const ExpiresInDefault: ExpiresInValue = { number: 30, interval: "day" };
|
||||
|
||||
const customExpiresFormat = (value: Date): string | null => {
|
||||
return formatDate(value);
|
||||
};
|
||||
|
||||
const SetupKeyNew = () => {
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const setupKey = useSelector((state: RootState) => state.setupKey.setupKey);
|
||||
const savedSetupKey = useSelector(
|
||||
(state: RootState) => state.setupKey.savedSetupKey
|
||||
);
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const [editName, setEditName] = useState(false);
|
||||
const [tagGroups, setTagGroups] = useState([] as string[]);
|
||||
const [formSetupKey, setFormSetupKey] = useState({} as FormSetupKey);
|
||||
const inputNameRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
//Unmounting component clean
|
||||
return () => {
|
||||
setVisibleNewSetupKey(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editName) return;
|
||||
|
||||
inputNameRef.current!.focus({ cursor: "end" });
|
||||
}, [editName]);
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(
|
||||
groups?.filter((g) => g.name !== "All").map((g) => g.name) || []
|
||||
);
|
||||
}, [groups]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!setupKey) return;
|
||||
|
||||
const allGroups = new Map<string, Group>();
|
||||
let formKeyGroups: string[] = [];
|
||||
groups.forEach((g) => allGroups.set(g.id!, g));
|
||||
|
||||
if (setupKey.auto_groups) {
|
||||
formKeyGroups = setupKey.auto_groups
|
||||
.filter((g) => allGroups.get(g))
|
||||
.map((g) => allGroups.get(g)!.name);
|
||||
}
|
||||
|
||||
const fSetupKey = {
|
||||
...setupKey,
|
||||
autoGroupNames: setupKey.auto_groups ? formKeyGroups : [],
|
||||
expiresInFormatted: ExpiresInDefault,
|
||||
exp: moment(setupKey.expires),
|
||||
last: moment(setupKey.last_used),
|
||||
} as FormSetupKey;
|
||||
|
||||
form.setFieldsValue(fSetupKey);
|
||||
setFormSetupKey(fSetupKey);
|
||||
}, [setupKey]);
|
||||
|
||||
const createSetupKeyToSave = (): SetupKeyToSave => {
|
||||
const autoGroups =
|
||||
groups
|
||||
?.filter((g) => formSetupKey.autoGroupNames.includes(g.name))
|
||||
.map((g) => g.id || "") || [];
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map((g) => g.name);
|
||||
const groupsToCreate = formSetupKey.autoGroupNames.filter(
|
||||
(s) => !allGroupsNames.includes(s)
|
||||
);
|
||||
|
||||
const expiresIn = expiresInToSeconds(formSetupKey.expiresInFormatted);
|
||||
return {
|
||||
id: formSetupKey.id,
|
||||
name: formSetupKey.name,
|
||||
type: formSetupKey.type,
|
||||
auto_groups: autoGroups,
|
||||
revoked: formSetupKey.revoked,
|
||||
groupsToCreate: groupsToCreate,
|
||||
expires_in: expiresIn,
|
||||
usage_limit: formSetupKey.usage_limit,
|
||||
} as SetupKeyToSave;
|
||||
};
|
||||
|
||||
const handleFormSubmit = async () => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
} catch (e) {
|
||||
const errorFields = (e as any).errorFields;
|
||||
return console.log("errorInfo", errorFields);
|
||||
}
|
||||
|
||||
const setupKeyToSave = createSetupKeyToSave();
|
||||
dispatch(
|
||||
setupKeyActions.saveSetupKey.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: setupKeyToSave,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const setVisibleNewSetupKey = (status: boolean) => {
|
||||
form.resetFields();
|
||||
dispatch(setupKeyActions.setSetupEditKeyVisible(status));
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedSetupKey.loading) return;
|
||||
|
||||
dispatch(
|
||||
setupKeyActions.setSetupKey({
|
||||
name: "",
|
||||
type: "one-off",
|
||||
key: "",
|
||||
last_used: "",
|
||||
expires: "",
|
||||
state: "valid",
|
||||
auto_groups: [] as string[],
|
||||
usage_limit: 0,
|
||||
used_times: 0,
|
||||
expires_in: 0,
|
||||
} as SetupKey)
|
||||
);
|
||||
setFormSetupKey({} as FormSetupKey);
|
||||
setVisibleNewSetupKey(false);
|
||||
};
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormSetupKey({ ...formSetupKey, ...data });
|
||||
};
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = [];
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(
|
||||
new Error("Group names with just spaces are not allowed")
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const { value, closable, onClose } = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color="blue"
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = "";
|
||||
const g = groups.find((_g) => _g.name === label);
|
||||
|
||||
if (g) {
|
||||
peersCount = ` - ${g.peers_count || 0} ${
|
||||
!g.peers_count || parseInt(g.peers_count) !== 1 ? "peers" : "peer"
|
||||
} `;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tag color="blue" style={{ marginRight: 3 }}>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{ fontSize: ".85em" }}>{peersCount}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: "8px 0" }} />
|
||||
<Row style={{ padding: "0 8px 4px" }}>
|
||||
<Col flex="auto">
|
||||
<span style={{ color: "#9CA3AF" }}>
|
||||
Add new group by pressing "Enter"
|
||||
</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg
|
||||
width="14"
|
||||
height="12"
|
||||
viewBox="0 0 14 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"
|
||||
/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
|
||||
const changesDetected = (): boolean => {
|
||||
return (
|
||||
formSetupKey.name == null ||
|
||||
formSetupKey.name !== setupKey.name ||
|
||||
groupsChanged() ||
|
||||
formSetupKey.usage_limit !== setupKey.usage_limit
|
||||
);
|
||||
};
|
||||
|
||||
const groupsChanged = (): boolean => {
|
||||
if (
|
||||
setupKey &&
|
||||
setupKey.auto_groups &&
|
||||
formSetupKey.autoGroupNames.length !== setupKey.auto_groups.length
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const formGroupIds =
|
||||
groups
|
||||
?.filter((g) => formSetupKey.autoGroupNames.includes(g.name))
|
||||
.map((g) => g.id || "") || [];
|
||||
|
||||
return (
|
||||
setupKey.auto_groups?.filter((g) => !formGroupIds.includes(g)).length > 0
|
||||
);
|
||||
};
|
||||
|
||||
const getFormKey = (key: string) => {
|
||||
if (key) return key.split("-")[0].concat("*****");
|
||||
};
|
||||
|
||||
const onBreadcrumbUsersClick = () => {
|
||||
if (savedSetupKey.loading) return;
|
||||
// dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null));
|
||||
setVisibleNewSetupKey(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb
|
||||
style={{ marginBottom: "30px" }}
|
||||
items={[
|
||||
{
|
||||
title: <a onClick={onBreadcrumbUsersClick}>Setup Keys</a>,
|
||||
},
|
||||
{
|
||||
title: setupKey.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card
|
||||
bordered={true}
|
||||
title={setupKey.name}
|
||||
style={{ marginBottom: "7px" }}
|
||||
>
|
||||
<div style={{ maxWidth: "800px" }}>
|
||||
<Form
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
form={form}
|
||||
onValuesChange={onChange}
|
||||
initialValues={{
|
||||
expiresIn: ExpiresInDefault,
|
||||
usage_limit: 1,
|
||||
}}
|
||||
>
|
||||
<Row style={{ marginTop: "10px" }}>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={11}
|
||||
lg={11}
|
||||
xl={11}
|
||||
xxl={11}
|
||||
span={11}
|
||||
style={{ paddingRight: "70px" }}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
fontWeight: "bold",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Key
|
||||
<Tag
|
||||
color={`${
|
||||
formSetupKey.state === "valid" ? "green" : "red"
|
||||
}`}
|
||||
style={{
|
||||
marginLeft: "10px",
|
||||
borderRadius: "2px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
{formSetupKey.state}
|
||||
</Tag>
|
||||
</Paragraph>
|
||||
<Input
|
||||
style={{ marginTop: "8px" }}
|
||||
disabled
|
||||
value={getFormKey(formSetupKey.key)}
|
||||
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
></Paragraph>
|
||||
{formSetupKey.type === "one-off" ? "One-off" : "Reusable"},
|
||||
available uses
|
||||
</Paragraph>
|
||||
<Col>
|
||||
<Input
|
||||
disabled
|
||||
value={
|
||||
formSetupKey.type === "reusable" &&
|
||||
formSetupKey.usage_limit === 0
|
||||
? "unlimited"
|
||||
: formSetupKey.usage_limit - formSetupKey.used_times
|
||||
}
|
||||
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
|
||||
style={{ marginTop: "8px" }}
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ marginTop: "30px" }}>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={11}
|
||||
lg={11}
|
||||
xl={11}
|
||||
xxl={11}
|
||||
span={11}
|
||||
style={{ paddingRight: "70px" }}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Auto-assigned groups
|
||||
</Paragraph>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
style={{ marginTop: "8px", marginBottom: 0 }}
|
||||
name="autoGroupNames"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Associate groups with the key"
|
||||
tagRender={tagRender}
|
||||
dropdownRender={dropDownRender}
|
||||
// enabled only when we have a new key !setupkey.id or when the key is valid
|
||||
disabled={!(!setupKey.id || setupKey.valid)}
|
||||
>
|
||||
{tagGroups.map((m) => (
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Paragraph style={{ margin: 0, fontWeight: "bold" }}>
|
||||
Expires
|
||||
</Paragraph>
|
||||
<Row>
|
||||
<Input
|
||||
style={{ marginTop: "8px" }}
|
||||
disabled
|
||||
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
|
||||
value={customExpiresFormat(new Date(formSetupKey.expires))!}
|
||||
/>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ marginTop: "40px", marginBottom: "28px" }}>
|
||||
<Text type={"secondary"}>
|
||||
Learn more about
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
||||
>
|
||||
{" "}
|
||||
setup keys
|
||||
</a>
|
||||
</Text>
|
||||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
<Container
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "start",
|
||||
padding: 0,
|
||||
gap: "10px",
|
||||
}}
|
||||
key={0}
|
||||
>
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={savedSetupKey.loading || !changesDetected()}
|
||||
onClick={handleFormSubmit}
|
||||
>
|
||||
{`${formSetupKey.id ? "Save" : "Create"} key`}
|
||||
</Button>
|
||||
</Container>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupKeyNew;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -297,238 +297,395 @@ const UserEdit = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{paddingTop: "13px"}}>
|
||||
<Breadcrumb style={{marginBottom: "30px"}}
|
||||
items={[
|
||||
{
|
||||
title: <a onClick={() => onBreadcrumbUsersClick("Users")}>All Users</a>,
|
||||
},
|
||||
{
|
||||
title: <a onClick={() => onBreadcrumbUsersClick(tab)}>{tab}</a>,
|
||||
// menu: { items: menuItems },
|
||||
},
|
||||
{
|
||||
title: user.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<>
|
||||
<div style={{ paddingTop: "13px" }}>
|
||||
<Breadcrumb
|
||||
style={{ marginBottom: "30px" }}
|
||||
items={[
|
||||
{
|
||||
title: (
|
||||
<a onClick={() => onBreadcrumbUsersClick("Users")}>
|
||||
All Users
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <a onClick={() => onBreadcrumbUsersClick(tab)}>{tab}</a>,
|
||||
// menu: { items: menuItems },
|
||||
},
|
||||
{
|
||||
title: user.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Card
|
||||
bordered={true}
|
||||
title={user.name}
|
||||
loading={loading}
|
||||
style={{marginBottom: "7px"}}
|
||||
>
|
||||
<div style={{maxWidth: "800px"}}>
|
||||
<Form layout="vertical" hideRequiredMark form={form}
|
||||
initialValues={{
|
||||
name: formUser.name,
|
||||
role: formUser.role,
|
||||
email: formUser.email,
|
||||
is_blocked: formUser.is_blocked,
|
||||
autoGroupsNames: formUser.autoGroupsNames,
|
||||
}}
|
||||
<Card
|
||||
bordered={true}
|
||||
title={user.name}
|
||||
loading={loading}
|
||||
style={{ marginBottom: "7px" }}
|
||||
>
|
||||
<div style={{ maxWidth: "800px" }}>
|
||||
<Form
|
||||
layout="vertical"
|
||||
hideRequiredMark
|
||||
form={form}
|
||||
initialValues={{
|
||||
name: formUser.name,
|
||||
role: formUser.role,
|
||||
email: formUser.email,
|
||||
is_blocked: formUser.is_blocked,
|
||||
autoGroupsNames: formUser.autoGroupsNames,
|
||||
}}
|
||||
>
|
||||
<Row style={{ paddingBottom: "15px" }}>
|
||||
{!user.is_service_user && (
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={11}
|
||||
lg={11}
|
||||
xl={11}
|
||||
xxl={11}
|
||||
span={11}
|
||||
>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label={<Text style={{}}>Email</Text>}
|
||||
style={{ marginRight: "70px", fontWeight: "bold" }}
|
||||
>
|
||||
<Input
|
||||
disabled={user.id}
|
||||
value={formUser.email}
|
||||
style={{ color: "#5a5c5a" }}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Form.Item
|
||||
name="role"
|
||||
label={<Text style={{}}>Role</Text>}
|
||||
style={{ marginRight: "50px", fontWeight: "bold" }}
|
||||
>
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
disabled={user.is_current}
|
||||
>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{!user.is_service_user && (
|
||||
<Row style={{ paddingBottom: "15px" }}>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={11}
|
||||
lg={11}
|
||||
xl={11}
|
||||
xxl={11}
|
||||
span={11}
|
||||
>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
label={<Text style={{}}>Auto-assigned groups</Text>}
|
||||
tooltip="Every peer enrolled with this user will be automatically added to these groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{ marginRight: "70px", fontWeight: "bold" }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="Associate groups with the user"
|
||||
tagRender={tagRender}
|
||||
dropdownRender={dropDownRender}
|
||||
disabled={oidcUser && !isUserAdmin(oidcUser.sub)}
|
||||
>
|
||||
<Row style={{paddingBottom: "15px"}}>
|
||||
{!user.is_service_user &&
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label={<Text style={{}}>Email</Text>}
|
||||
style={{marginRight: "70px"}}
|
||||
>
|
||||
<Input
|
||||
disabled={user.id}
|
||||
value={formUser.email}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>}
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Form.Item
|
||||
name="role"
|
||||
label={<Text style={{}}>Role</Text>}
|
||||
style={{marginRight: "50px"}}
|
||||
>
|
||||
<Select style={{width: '100%'}}
|
||||
disabled={user.is_current}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
{tagGroups.map((m) => (
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
{!user.is_service_user && <Row style={{paddingBottom: "15px"}}>
|
||||
{!user.is_current && isAdmin && (
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5}
|
||||
span={5}
|
||||
>
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
name="is_blocked"
|
||||
label="Block user"
|
||||
style={{ marginRight: "50px", fontWeight: "bold" }}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
label={<Text style={{}}>Auto-assigned groups</Text>}
|
||||
tooltip="Every peer enrolled with this user will be automatically added to these groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
style={{marginRight: "70px"}}
|
||||
>
|
||||
<Select mode="tags"
|
||||
placeholder="Associate groups with the user"
|
||||
tagRender={tagRender}
|
||||
dropdownRender={dropDownRender}
|
||||
disabled={oidcUser && !isUserAdmin(oidcUser.sub)}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
{!user.is_current && isAdmin && (
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
name="is_blocked"
|
||||
label="Block user"
|
||||
style={{marginRight: "50px"}}
|
||||
>
|
||||
<Switch/>
|
||||
</Form.Item>
|
||||
</Col>)}
|
||||
</Row>}
|
||||
|
||||
<Space style={{display: 'flex', justifyContent: 'start'}}>
|
||||
<Button disabled={loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary"
|
||||
onClick={handleFormSubmit}>Save</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{user && (user.is_current || user.is_service_user) && <Card
|
||||
bordered={true}
|
||||
loading={loading}
|
||||
style={{marginBottom: "7px"}}
|
||||
>
|
||||
<div style={{maxWidth: "800px"}}>
|
||||
<Paragraph
|
||||
style={{textAlign: "left", whiteSpace: "pre-line", fontSize: "16px", fontWeight: "bold"}}>Access
|
||||
tokens</Paragraph>
|
||||
<Row gutter={21} style={{marginTop: "-16px", marginBottom: "10px"}}>
|
||||
<Col xs={24} sm={24} md={20} lg={20} xl={20} xxl={20} span={20}>
|
||||
<Paragraph type={"secondary"}
|
||||
style={{textAlign: "left", whiteSpace: "pre-line"}}>
|
||||
Access tokens give access to NetBird API</Paragraph>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={1} lg={1} xl={1} xxl={1} span={1} style={{marginTop: "-16px"}}>
|
||||
{personalAccessTokens && personalAccessTokens.length > 0 &&
|
||||
<Button type="primary" onClick={onClickAddNewPersonalAccessToken}>Create
|
||||
token</Button>}
|
||||
</Col>
|
||||
</Row>
|
||||
{personalAccessTokens && personalAccessTokens.length > 0 &&
|
||||
<Table
|
||||
size={"small"}
|
||||
style={{marginTop: "-10px"}}
|
||||
showHeader={false}
|
||||
scroll={{x: 800}}
|
||||
pagination={false}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={tokenTable}>
|
||||
<Column className={"non-highlighted-table-column"}
|
||||
sorter={(a, b) => ((a as TokenDataTable).created_at.localeCompare((b as TokenDataTable).created_at))}
|
||||
defaultSortOrder='descend'
|
||||
render={(text, record, index) => {
|
||||
return (<>
|
||||
<Row>
|
||||
<Col>
|
||||
<Badge
|
||||
status={(record as TokenDataTable).status === "valid" ? "success" : "error"}
|
||||
style={{
|
||||
marginTop: "1px",
|
||||
marginRight: "5px",
|
||||
marginLeft: "0px"
|
||||
}}/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Paragraph style={{
|
||||
margin: "0px",
|
||||
padding: "0px"
|
||||
}}>{(record as TokenDataTable).name}</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{
|
||||
fontSize: "13px",
|
||||
fontWeight: "400",
|
||||
margin: "0px",
|
||||
marginTop: "-2px",
|
||||
padding: "0px"
|
||||
}}>{"Created" + ((record as TokenDataTable).created_by_email && user.is_service_user ? " by " + (record as TokenDataTable).created_by_email : "") + " on " + fullDate((record as TokenDataTable).created_at)}</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</>)
|
||||
}}/>
|
||||
<Column render={(text, record, index) => {
|
||||
return <>
|
||||
<Paragraph type={"secondary"} style={{textAlign: "left", fontSize: "11px"}}>Expires
|
||||
on</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{
|
||||
textAlign: "left",
|
||||
marginTop: "-10px",
|
||||
marginBottom: "0",
|
||||
fontSize: "15px"
|
||||
}}>{fullDate((record as TokenDataTable).expiration_date)}</Paragraph>
|
||||
</>
|
||||
}}
|
||||
/>
|
||||
<Column render={(text, record, index) => {
|
||||
return <>
|
||||
<Paragraph type={"secondary"} style={{textAlign: "left", fontSize: "11px"}}>Last
|
||||
used</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{
|
||||
textAlign: "left",
|
||||
marginTop: "-10px",
|
||||
marginBottom: "0",
|
||||
fontSize: "15px"
|
||||
}}>{(record as TokenDataTable).last_used ? fullDate((record as TokenDataTable).last_used) : "Never"}</Paragraph>
|
||||
</>
|
||||
}}
|
||||
/>
|
||||
<Column align="right"
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<Button danger={true} type={"text"}
|
||||
onClick={() => {
|
||||
showConfirmDelete(record as TokenDataTable)
|
||||
}}
|
||||
>Delete</Button>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Table>}
|
||||
<Divider style={{marginTop: "-12px"}}></Divider>
|
||||
{(personalAccessTokens === null || personalAccessTokens.length === 0) &&
|
||||
<Space direction="vertical" size="small" align="start"
|
||||
style={{
|
||||
display: 'flex',
|
||||
padding: '35px 0px',
|
||||
marginTop: "-40px",
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<Paragraph
|
||||
style={{textAlign: "start", whiteSpace: "pre-line"}}>
|
||||
You don’t have any access tokens yet
|
||||
</Paragraph>
|
||||
<Button type="primary" onClick={onClickAddNewPersonalAccessToken}>Create token</Button>
|
||||
</Space>}
|
||||
</div>
|
||||
|
||||
</Card>}
|
||||
<Space style={{ display: "flex", justifyContent: "start" }}>
|
||||
<Button disabled={loading} onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleFormSubmit}>
|
||||
Save
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
<AddPATPopup/>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
</Card>
|
||||
|
||||
{user && (user.is_current || user.is_service_user) && (
|
||||
<Card
|
||||
bordered={true}
|
||||
loading={loading}
|
||||
style={{ marginBottom: "7px" }}
|
||||
>
|
||||
<div style={{ maxWidth: "800px" }}>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "left",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Access tokens
|
||||
</Paragraph>
|
||||
<Row
|
||||
gutter={21}
|
||||
style={{ marginTop: "-16px", marginBottom: "10px" }}
|
||||
>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={20}
|
||||
lg={20}
|
||||
xl={20}
|
||||
xxl={20}
|
||||
span={20}
|
||||
>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{ textAlign: "left", whiteSpace: "pre-line" }}
|
||||
>
|
||||
Access tokens give access to NetBird API
|
||||
</Paragraph>
|
||||
</Col>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={1}
|
||||
lg={1}
|
||||
xl={1}
|
||||
xxl={1}
|
||||
span={1}
|
||||
style={{ marginTop: "-16px" }}
|
||||
>
|
||||
{personalAccessTokens &&
|
||||
personalAccessTokens.length > 0 && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onClickAddNewPersonalAccessToken}
|
||||
>
|
||||
Create token
|
||||
</Button>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
{personalAccessTokens && personalAccessTokens.length > 0 && (
|
||||
<Table
|
||||
size={"small"}
|
||||
style={{ marginTop: "-10px" }}
|
||||
showHeader={false}
|
||||
scroll={{ x: 800 }}
|
||||
pagination={false}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={tokenTable}
|
||||
>
|
||||
<Column
|
||||
className={"non-highlighted-table-column"}
|
||||
sorter={(a, b) =>
|
||||
(a as TokenDataTable).created_at.localeCompare(
|
||||
(b as TokenDataTable).created_at
|
||||
)
|
||||
}
|
||||
defaultSortOrder="descend"
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Col>
|
||||
<Badge
|
||||
status={
|
||||
(record as TokenDataTable).status ===
|
||||
"valid"
|
||||
? "success"
|
||||
: "error"
|
||||
}
|
||||
style={{
|
||||
marginTop: "1px",
|
||||
marginRight: "5px",
|
||||
marginLeft: "0px",
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Paragraph
|
||||
style={{
|
||||
margin: "0px",
|
||||
padding: "0px",
|
||||
}}
|
||||
>
|
||||
{(record as TokenDataTable).name}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
fontWeight: "400",
|
||||
margin: "0px",
|
||||
marginTop: "-2px",
|
||||
padding: "0px",
|
||||
}}
|
||||
>
|
||||
{"Created" +
|
||||
((record as TokenDataTable)
|
||||
.created_by_email && user.is_service_user
|
||||
? " by " +
|
||||
(record as TokenDataTable)
|
||||
.created_by_email
|
||||
: "") +
|
||||
" on " +
|
||||
fullDate(
|
||||
(record as TokenDataTable).created_at
|
||||
)}
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{ textAlign: "left", fontSize: "11px" }}
|
||||
>
|
||||
Expires on
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
marginTop: "-10px",
|
||||
marginBottom: "0",
|
||||
fontSize: "15px",
|
||||
}}
|
||||
>
|
||||
{fullDate(
|
||||
(record as TokenDataTable).expiration_date
|
||||
)}
|
||||
</Paragraph>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{ textAlign: "left", fontSize: "11px" }}
|
||||
>
|
||||
Last used
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
marginTop: "-10px",
|
||||
marginBottom: "0",
|
||||
fontSize: "15px",
|
||||
}}
|
||||
>
|
||||
{(record as TokenDataTable).last_used
|
||||
? fullDate((record as TokenDataTable).last_used)
|
||||
: "Never"}
|
||||
</Paragraph>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
align="right"
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<Button
|
||||
danger={true}
|
||||
type={"text"}
|
||||
onClick={() => {
|
||||
showConfirmDelete(record as TokenDataTable);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
)}
|
||||
<Divider style={{ marginTop: "-12px" }}></Divider>
|
||||
{(personalAccessTokens === null ||
|
||||
personalAccessTokens.length === 0) && (
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="small"
|
||||
align="start"
|
||||
style={{
|
||||
display: "flex",
|
||||
padding: "35px 0px",
|
||||
marginTop: "-40px",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{ textAlign: "start", whiteSpace: "pre-line" }}
|
||||
>
|
||||
You don’t have any access tokens yet
|
||||
</Paragraph>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onClickAddNewPersonalAccessToken}
|
||||
>
|
||||
Create token
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
<AddPATPopup />
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ const actions = {
|
||||
|
||||
removeSetupKey: createAction('REMOVE_SETUP_KEY')<string>(),
|
||||
setSetupKey: createAction('SET_SETUP_KEY')<SetupKey>(),
|
||||
setSetupNewKeyVisible: createAction('SET_SETUP_NEW_KEY_VISIBLE')<boolean>()
|
||||
setSetupNewKeyVisible: createAction('SET_SETUP_NEW_KEY_VISIBLE')<boolean>(),
|
||||
setSetupEditKeyVisible: createAction('SET_SETUP_EDIT_KEY_VISIBLE')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
|
||||
@@ -5,15 +5,16 @@ 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>;
|
||||
savedSetupKey: CreateResponse<SetupKey | null>;
|
||||
setupNewKeyVisible: boolean
|
||||
data: SetupKey[] | null;
|
||||
setupKey: SetupKey | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
deletedSetupKey: DeleteResponse<string | null>;
|
||||
revokedSetupKey: ChangeResponse<SetupKey | null>;
|
||||
savedSetupKey: CreateResponse<SetupKey | null>;
|
||||
setupNewKeyVisible: boolean;
|
||||
setupEditKeyVisible: boolean;
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
@@ -43,7 +44,8 @@ const initialState: StateType = {
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
setupNewKeyVisible: false
|
||||
setupNewKeyVisible: false,
|
||||
setupEditKeyVisible: false
|
||||
};
|
||||
|
||||
const data = createReducer<SetupKey[], ActionTypes>(initialState.data as SetupKey[])
|
||||
@@ -85,13 +87,17 @@ const savedSetupKey = createReducer<CreateResponse<SetupKey | null>, ActionTypes
|
||||
const setupNewKeyVisible = createReducer<boolean, ActionTypes>(initialState.setupNewKeyVisible)
|
||||
.handleAction(actions.setSetupNewKeyVisible, (store, action) => action.payload)
|
||||
|
||||
const setupEditKeyVisible = createReducer<boolean, ActionTypes>(initialState.setupEditKeyVisible)
|
||||
.handleAction(actions.setSetupEditKeyVisible, (store, action) => action.payload)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
setupKey,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedSetupKey,
|
||||
savedSetupKey: savedSetupKey,
|
||||
setupNewKeyVisible
|
||||
data,
|
||||
setupKey,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedSetupKey,
|
||||
savedSetupKey: savedSetupKey,
|
||||
setupNewKeyVisible,
|
||||
setupEditKeyVisible,
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface SetupKey {
|
||||
valid: boolean;
|
||||
auto_groups: string[]
|
||||
expires_in: number;
|
||||
usage_limit: number;
|
||||
usage_limit: any;
|
||||
}
|
||||
|
||||
export interface FormSetupKey extends SetupKey {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user