Update setup key edit layout (#190)

https://github.com/netbirdio/dashboard/issues/187
This commit is contained in:
Sarooj bukhari
2023-06-01 12:49:10 +05:00
committed by GitHub
parent 53ed514803
commit a7f64d4a15
7 changed files with 2075 additions and 1328 deletions

View 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

View File

@@ -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 dont 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 dont have any access tokens yet
</Paragraph>
<Button
type="primary"
onClick={onClickAddNewPersonalAccessToken}
>
Create token
</Button>
</Space>
)}
</div>
</Card>
)}
</div>
<AddPATPopup />
{confirmModalContextHolder}
</>
);
}

View File

@@ -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>;

View File

@@ -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,
});

View File

@@ -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