mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
474 lines
21 KiB
TypeScript
474 lines
21 KiB
TypeScript
import React, {useEffect, useState} from 'react';
|
|
import {useDispatch, useSelector} from "react-redux";
|
|
import {RootState} from "typesafe-actions";
|
|
import {actions as setupKeyActions} from '../store/setup-key';
|
|
import {Container} from "../components/Container";
|
|
import {
|
|
Alert,
|
|
Button,
|
|
Card,
|
|
Col,
|
|
Dropdown,
|
|
Input,
|
|
Menu,
|
|
message,
|
|
Modal,
|
|
Popover,
|
|
Radio,
|
|
RadioChangeEvent,
|
|
Row,
|
|
Select,
|
|
Space,
|
|
Table,
|
|
Tag,
|
|
Typography
|
|
} from "antd";
|
|
import {SetupKey, SetupKeyToSave} from "../store/setup-key/types";
|
|
import {filter} from "lodash"
|
|
import {formatDate, timeAgo} from "../utils/common";
|
|
import {ExclamationCircleOutlined} from "@ant-design/icons";
|
|
import SetupKeyNew from "../components/SetupKeyNew";
|
|
import ButtonCopyMessage from "../components/ButtonCopyMessage";
|
|
import tableSpin from "../components/Spin";
|
|
import {actions as groupActions} from "../store/group";
|
|
import {Group} from "../store/group/types";
|
|
import {TooltipPlacement} from "antd/es/tooltip";
|
|
import {useGetAccessTokenSilently} from "../utils/token";
|
|
|
|
const {Title, Text, Paragraph} = Typography;
|
|
const {Column} = Table;
|
|
const {confirm} = Modal;
|
|
|
|
interface SetupKeyDataTable extends SetupKey {
|
|
key: string
|
|
groupsCount: number
|
|
}
|
|
|
|
export const SetupKeys = () => {
|
|
//const {accessToken} = useOidcAccessToken()
|
|
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
|
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 savedSetupKey = useSelector((state: RootState) => state.setupKey.savedSetupKey);
|
|
const groups = useSelector((state: RootState) => state.group.data)
|
|
|
|
const [textToSearch, setTextToSearch] = useState('');
|
|
const [optionValidAll, setOptionValidAll] = useState('valid');
|
|
const [pageSize, setPageSize] = useState(10);
|
|
const [dataTable, setDataTable] = useState([] as SetupKeyDataTable[]);
|
|
const [setupKeyToAction, setSetupKeyToAction] = useState(null as SetupKeyDataTable | null);
|
|
const setupNewKeyVisible = useSelector((state: RootState) => state.setupKey.setupNewKeyVisible)
|
|
const [groupPopupVisible,setGroupPopupVisible] = useState(false as boolean|undefined)
|
|
|
|
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: "edit",
|
|
label: (<Button type="text" onClick={() => onClickEditSetupKey()}>View</Button>)
|
|
},
|
|
|
|
]
|
|
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
|
|
|
const transformDataTable = (d: SetupKey[]): SetupKeyDataTable[] => {
|
|
return d.map(p => ({...p, groupsCount: p.auto_groups ? p.auto_groups.length : 0} as SetupKeyDataTable))
|
|
}
|
|
|
|
useEffect(() => {
|
|
dispatch(setupKeyActions.getSetupKeys.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
|
dispatch(groupActions.getGroups.request({getAccessTokenSilently: 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: 'Setup key has been successfully removed.',
|
|
key: deleteKey,
|
|
duration: 2,
|
|
style: styleNotification
|
|
});
|
|
dispatch(setupKeyActions.setDeleteSetupKey({...deletedSetupKey, success: false}))
|
|
dispatch(setupKeyActions.resetDeletedSetupKey(null))
|
|
} else if (deletedSetupKey.error) {
|
|
message.error({
|
|
content: 'Failed to delete setup key. You might not have enough permissions.',
|
|
key: deleteKey,
|
|
duration: 2,
|
|
style: styleNotification
|
|
});
|
|
dispatch(setupKeyActions.setDeleteSetupKey({...deletedSetupKey, error: null}))
|
|
dispatch(setupKeyActions.resetDeletedSetupKey(null))
|
|
}
|
|
}, [deletedSetupKey])
|
|
|
|
const createKey = 'saving';
|
|
useEffect(() => {
|
|
if (savedSetupKey.loading) {
|
|
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
|
} else if (savedSetupKey.success) {
|
|
message.success({
|
|
content: 'Setup key has been successfully saved.',
|
|
key: createKey,
|
|
duration: 2,
|
|
style: styleNotification
|
|
});
|
|
dispatch(setupKeyActions.setSetupNewKeyVisible(false));
|
|
dispatch(setupKeyActions.setSavedSetupKey({...savedSetupKey, success: false}));
|
|
dispatch(setupKeyActions.resetSavedSetupKey(null))
|
|
} else if (savedSetupKey.error) {
|
|
message.error({
|
|
content: 'Failed to update setup key. You might not have enough permissions.',
|
|
key: createKey,
|
|
duration: 2,
|
|
style: styleNotification
|
|
});
|
|
dispatch(setupKeyActions.setSavedSetupKey({...savedSetupKey, error: null}));
|
|
dispatch(setupKeyActions.resetSavedSetupKey(null))
|
|
}
|
|
}, [savedSetupKey])
|
|
|
|
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.toLowerCase().includes(t) || _f.key.toLowerCase().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 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.saveSetupKey.request({
|
|
getAccessTokenSilently: getAccessTokenSilently,
|
|
payload: {
|
|
id: setupKeyToAction ? setupKeyToAction.id : null,
|
|
revoked: true,
|
|
name: setupKeyToAction ? setupKeyToAction.name : null,
|
|
auto_groups: setupKeyToAction && setupKeyToAction.auto_groups ? setupKeyToAction.auto_groups : [],
|
|
} as SetupKeyToSave
|
|
}));
|
|
},
|
|
onCancel() {
|
|
setSetupKeyToAction(null);
|
|
},
|
|
});
|
|
}
|
|
|
|
const onClickAddNewSetupKey = () => {
|
|
const autoGroups : string[] = []
|
|
dispatch(setupKeyActions.setSetupNewKeyVisible(true));
|
|
dispatch(setupKeyActions.setSetupKey({
|
|
name: "",
|
|
type: "reusable",
|
|
auto_groups: autoGroups
|
|
} as SetupKey))
|
|
}
|
|
|
|
const setKeyAndView = (key: SetupKeyDataTable) => {
|
|
dispatch(setupKeyActions.setSetupNewKeyVisible(true));
|
|
dispatch(setupKeyActions.setSetupKey({
|
|
id: key?.id || null,
|
|
key: key?.key,
|
|
name: key?.name,
|
|
revoked: key?.revoked,
|
|
expires: key?.expires,
|
|
state: key?.state,
|
|
type: key?.type,
|
|
used_times: key?.used_times,
|
|
valid: key?.valid,
|
|
auto_groups: key?.auto_groups,
|
|
last_used: key?.last_used,
|
|
} as SetupKey))
|
|
}
|
|
|
|
const onClickEditSetupKey = () => {
|
|
dispatch(setupKeyActions.setSetupNewKeyVisible(true));
|
|
dispatch(setupKeyActions.setSetupKey({
|
|
id: setupKeyToAction?.id || null,
|
|
key: setupKeyToAction?.key,
|
|
name: setupKeyToAction?.name,
|
|
revoked: setupKeyToAction?.revoked,
|
|
expires: setupKeyToAction?.expires,
|
|
state: setupKeyToAction?.state,
|
|
type: setupKeyToAction?.type,
|
|
used_times: setupKeyToAction?.used_times,
|
|
valid: setupKeyToAction?.valid,
|
|
auto_groups: setupKeyToAction?.auto_groups,
|
|
last_used: setupKeyToAction?.last_used,
|
|
} as SetupKey))
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (setupNewKeyVisible) {
|
|
setGroupPopupVisible(false)
|
|
}
|
|
}, [setupNewKeyVisible])
|
|
|
|
const onPopoverVisibleChange = (b:boolean) => {
|
|
if (setupNewKeyVisible) {
|
|
setGroupPopupVisible(false)
|
|
} else {
|
|
setGroupPopupVisible(undefined)
|
|
}
|
|
}
|
|
|
|
const renderPopoverGroups = (label: string, rowGroups: string[] | string[] | null, setupKeyToAction: SetupKeyDataTable) => {
|
|
|
|
let groupsMap = new Map<string, Group>();
|
|
groups.forEach(g => {
|
|
groupsMap.set(g.id!, g)
|
|
})
|
|
|
|
let displayGroups :Group[] = []
|
|
if (rowGroups) {
|
|
displayGroups = rowGroups.filter(g => groupsMap.get(g)).map(g => groupsMap.get(g)!)
|
|
}
|
|
|
|
let btn = <Button type="link" onClick={() => setUpdateGroupsVisible(setupKeyToAction, true)}>{displayGroups.length}</Button>
|
|
if (!displayGroups || displayGroups!.length < 1) {
|
|
return btn
|
|
}
|
|
|
|
const content = displayGroups?.map((g, i) => {
|
|
const _g = g as Group
|
|
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
|
return (
|
|
<div key={i}>
|
|
<Tag
|
|
color="blue"
|
|
style={{marginRight: 3}}
|
|
>
|
|
<strong>{_g.name}</strong>
|
|
</Tag>
|
|
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
|
</div>
|
|
)
|
|
})
|
|
const mainContent = (<Space direction="vertical">{content}</Space>)
|
|
let popoverPlacement = "top"
|
|
if (content && content.length > 5) {
|
|
popoverPlacement = "rightTop"
|
|
}
|
|
|
|
return (
|
|
<Popover placement={popoverPlacement as TooltipPlacement}
|
|
key={setupKeyToAction.key}
|
|
onVisibleChange={onPopoverVisibleChange}
|
|
visible={groupPopupVisible}
|
|
content={mainContent}
|
|
title={null}>
|
|
{btn}
|
|
</Popover>
|
|
)
|
|
}
|
|
|
|
const setUpdateGroupsVisible = (setupKeyToAction: SetupKey, status: boolean) => {
|
|
if (status) {
|
|
dispatch(setupKeyActions.setSetupKey({...setupKeyToAction}))
|
|
dispatch(setupKeyActions.setSetupNewKeyVisible(true))
|
|
return
|
|
}
|
|
const autoGroups : string[] = []
|
|
dispatch(setupKeyActions.setSetupKey({
|
|
name: "",
|
|
type: "reusable",
|
|
auto_groups: autoGroups
|
|
} as SetupKey))
|
|
dispatch(setupKeyActions.setSetupNewKeyVisible(false))
|
|
}
|
|
|
|
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/>
|
|
}
|
|
<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}}
|
|
loading={tableSpin(loading)}
|
|
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))}
|
|
render={(text, record, index) => {
|
|
return <Button type="text"
|
|
onClick={() => setKeyAndView(record as SetupKeyDataTable)}
|
|
className="tooltip-label"> <Text strong>{text}</Text>
|
|
</Button>
|
|
}}
|
|
defaultSortOrder='ascend'
|
|
/>
|
|
|
|
<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="Groups" dataIndex="groupsCount" align="center"
|
|
render={(text, record: SetupKeyDataTable, index) => {
|
|
return renderPopoverGroups(text, record.auto_groups, record)
|
|
}}
|
|
/>
|
|
<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="last_used"
|
|
sorter={(a, b) => ((a as any).last_used.localeCompare((b as any).last_used))}
|
|
render={(text, record, index) => {
|
|
return !(record as SetupKey).used_times ? 'never' : timeAgo(text)
|
|
}}
|
|
/>
|
|
<Column title="Used Times" dataIndex="used_times"
|
|
sorter={(a, b) => ((a as any).used_times - ((b as any).used_times))}
|
|
/>
|
|
|
|
<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 SetupKeys; |