antd/lib/table#ColumnsType TypeScript Examples
The following examples show how to use
antd/lib/table#ColumnsType.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: ExportTableButton.tsx From ant-table-extensions with MIT License | 6 votes |
getFieldsFromColumns = (
columns: ColumnsType<any>
): ITableExportFields => {
const fields = {};
columns?.forEach((column: ColumnWithDataIndex) => {
const { title, key, dataIndex } = column;
const fieldName =
(Array.isArray(dataIndex) ? dataIndex.join(".") : dataIndex) ?? key;
if (fieldName) {
set(fields, fieldName, title);
}
});
return fields;
}
Example #2
Source File: Members.tsx From subscan-multisig-react with Apache License 2.0 | 5 votes |
Members = ({ record }: { record: KeyringAddress }) => { const { t } = useTranslation(); const isExtensionAccount = useIsInjected(); const columnsNested: ColumnsType<KeyringJson> = [ { dataIndex: 'name', render: (name) => <div>{name}</div>, }, { dataIndex: 'address', render: (address) => ( <Space size="small" className="flex items-center"> <BaseIdentityIcon theme="polkadot" size={32} value={address} /> <SubscanLink address={address} /> </Space> ), }, { key: 'isInject', render: (_, pair) => t(isExtensionAccount(pair.address) ? 'injected' : 'external'), }, ]; return ( <div className="bg-gray-100 small-table" style={{ padding: '20px', }} > <Table columns={columnsNested} dataSource={record.meta.addressPair as KeyringJson[]} pagination={false} bordered rowKey="address" className="table-without-head hidden lg:block" /> <MemberList data={record} /> </div> ); }
Example #3
Source File: PersonalAPIKeys.tsx From posthog-foss with MIT License | 5 votes |
function PersonalAPIKeysTable(): JSX.Element {
const { keys } = useValues(personalAPIKeysLogic) as { keys: PersonalAPIKeyType[] }
const { deleteKey } = useActions(personalAPIKeysLogic)
const columns: ColumnsType<Record<string, any>> = [
{
title: 'Label',
dataIndex: 'label',
key: 'label',
},
{
title: 'Value',
dataIndex: 'value',
key: 'value',
className: 'ph-no-capture',
render: RowValue,
},
{
title: 'Last Used',
dataIndex: 'last_used_at',
key: 'lastUsedAt',
render: (lastUsedAt: string | null) => humanFriendlyDetailedTime(lastUsedAt),
},
{
title: 'Created',
dataIndex: 'created_at',
key: 'createdAt',
render: (createdAt: string | null) => humanFriendlyDetailedTime(createdAt),
},
{
title: '',
key: 'actions',
align: 'center',
render: RowActionsCreator(deleteKey),
},
]
return (
<Table
dataSource={keys}
columns={columns}
rowKey="id"
pagination={{ pageSize: 50, hideOnSinglePage: true }}
style={{ marginTop: '1rem' }}
/>
)
}
Example #4
Source File: Args.tsx From subscan-multisig-react with Apache License 2.0 | 5 votes |
export function Args({ args, className, section, method }: ArgsProps) {
const { t } = useTranslation();
const { chain } = useApi();
const columns: ColumnsType<ArgObj> = [
{
key: 'name',
dataIndex: 'name',
render(name: string, record) {
return name || record.type;
},
},
{
key: 'value',
dataIndex: 'value',
className: 'value-column',
// eslint-disable-next-line complexity
render(value, record) {
const { type, name } = record;
const isAddr = type ? isAddressType(type) : isSS58Address(value);
if (isObject(value)) {
return (
<Args
args={Object.entries(value).map(([prop, propValue]) => ({ name: prop, value: propValue }))}
section={section}
method={method}
/>
);
// return JSON.stringify(value);
}
if (isAddr) {
return formatAddressValue(value, chain);
}
// balances(transfer) kton(transfer)
if (isBalanceType(type || name) || isCrabValue(name) || section === 'balances' || method === 'transfer') {
const formatValue = toString(value).replaceAll(',', '');
return formatBalance(formatValue, +chain.tokens[0].decimal, {
noDecimal: false,
withThousandSplit: true,
}); // FIXME: decimal issue;
}
if (isDownloadType(value)) {
return (
<a href={value}>
{t('download')} <DownloadOutlined />
</a>
);
}
if (isValueType(name)) {
return value;
}
return <div style={{ wordBreak: 'break-all' }}>{value}</div>;
},
},
];
return (
<Table
columns={columns}
/* antd form data source require object array */
dataSource={args.map((arg) => (isObject(arg) ? arg : { value: arg }))}
pagination={false}
bordered
rowKey="name"
showHeader={false}
className={className}
/>
);
}
Example #5
Source File: InternalMetricsTab.tsx From posthog-foss with MIT License | 5 votes |
function QueryTable(props: {
showAnalyze?: boolean
queries?: QuerySummary[]
loading: boolean
columnExtra?: Record<string, any>
}): JSX.Element {
const { openAnalyzeModalWithQuery } = useActions(systemStatusLogic)
const columns: ColumnsType<QuerySummary> = [
{
title: 'duration',
dataIndex: 'duration',
key: 'duration',
sorter: (a, b) => +a.duration - +b.duration,
},
{
title: 'query',
dataIndex: 'query',
render: function RenderAnalyze({}, item: QuerySummary) {
if (!props.showAnalyze) {
return item.query
}
return (
<Link to="#" onClick={() => openAnalyzeModalWithQuery(item.query)}>
{item.query}
</Link>
)
},
key: 'query',
},
]
if (props.queries && props.queries.length > 0) {
Object.keys(props.queries[0]).forEach((column) => {
if (column !== 'duration' && column !== 'query') {
columns.push({ title: column, dataIndex: column, key: column })
}
})
}
return (
<Table
dataSource={props.queries || []}
columns={columns}
loading={props.loading}
pagination={{ pageSize: 30, hideOnSinglePage: true }}
size="small"
bordered
style={{ overflowX: 'auto', overflowY: 'auto' }}
locale={{ emptyText: 'No queries found' }}
/>
)
}
Example #6
Source File: Members.tsx From posthog-foss with MIT License | 5 votes |
export function Members({ user }: MembersProps): JSX.Element {
const { members, membersLoading } = useValues(membersLogic)
const columns: ColumnsType<OrganizationMemberType> = [
{
key: 'user_profile_picture',
render: function ProfilePictureRender(_, member) {
return <ProfilePicture name={member.user.first_name} email={member.user.email} />
},
width: 32,
},
{
title: 'Name',
key: 'user_first_name',
render: (_, member) =>
member.user.uuid == user.uuid ? `${member.user.first_name} (me)` : member.user.first_name,
sorter: (a, b) => a.user.first_name.localeCompare(b.user.first_name),
},
{
title: 'Email',
key: 'user_email',
render: (_, member) => member.user.email,
sorter: (a, b) => a.user.email.localeCompare(b.user.email),
},
{
title: 'Level',
dataIndex: 'level',
key: 'level',
render: function LevelRender(_, member) {
return LevelComponent(member)
},
sorter: (a, b) => a.level - b.level,
defaultSortOrder: 'descend',
},
{
title: 'Joined At',
dataIndex: 'joined_at',
key: 'joined_at',
render: (joinedAt: string) => humanFriendlyDetailedTime(joinedAt),
sorter: (a, b) => a.joined_at.localeCompare(b.joined_at),
defaultSortOrder: 'ascend',
},
{
dataIndex: 'actions',
key: 'actions',
align: 'center',
render: function ActionsRender(_, member) {
return ActionsComponent(member)
},
},
]
return (
<>
<h2 className="subtitle">Members</h2>
<Table
dataSource={members}
columns={columns}
rowKey="id"
pagination={false}
style={{ marginTop: '1rem' }}
loading={membersLoading}
data-attr="org-members-table"
/>
</>
)
}
Example #7
Source File: TeamMembers.tsx From posthog-foss with MIT License | 5 votes |
export function TeamMembers({ user }: MembersProps): JSX.Element {
const { allMembers, allMembersLoading } = useValues(teamMembersLogic)
const columns: ColumnsType<FusedTeamMemberType> = [
{
key: 'user_profile_picture',
render: function ProfilePictureRender(_, member) {
return <ProfilePicture name={member.user.first_name} email={member.user.email} />
},
width: 32,
},
{
title: 'Name',
key: 'user_first_name',
render: (_, member) =>
member.user.uuid == user.uuid ? `${member.user.first_name} (me)` : member.user.first_name,
sorter: (a, b) => a.user.first_name.localeCompare(b.user.first_name),
},
{
title: 'Email',
key: 'user_email',
render: (_, member) => member.user.email,
sorter: (a, b) => a.user.email.localeCompare(b.user.email),
},
{
title: 'Level',
key: 'level',
render: function LevelRender(_, member) {
return LevelComponent(member)
},
sorter: (a, b) => a.level - b.level,
defaultSortOrder: 'descend',
},
{
title: 'Joined At',
dataIndex: 'joined_at',
key: 'joined_at',
render: (joinedAt: string) => humanFriendlyDetailedTime(joinedAt),
sorter: (a, b) => a.joined_at.localeCompare(b.joined_at),
defaultSortOrder: 'ascend',
},
{
key: 'actions',
align: 'center',
render: function ActionsRender(_, member) {
return ActionsComponent(member)
},
},
]
return (
<>
<h2 className="subtitle" id="members-with-project-access" style={{ justifyContent: 'space-between' }}>
Members with Project Access
<RestrictedArea
Component={AddMembersModalWithButton}
minimumAccessLevel={OrganizationMembershipLevel.Admin}
scope={RestrictionScope.Project}
/>
</h2>
<Table
dataSource={allMembers}
columns={columns}
rowKey="id"
pagination={false}
style={{ marginTop: '1rem' }}
loading={allMembersLoading}
data-attr="team-members-table"
/>
</>
)
}
Example #8
Source File: resourceTable.tsx From fe-v5 with Apache License 2.0 | 5 votes |
function ResourceTable({ onSelect }: IProps) {
const { t } = useTranslation();
const [selectRowKeys, setSelectRowKeys] = useState<React.Key[]>([]);
const { currentGroup } = useSelector<RootState, resourceStoreState>(
(state) => state.resource,
);
const [query, setQuery] = useState<string>('');
const columns: ColumnsType<resourceItem> = [
{
title: t('资源标识'),
dataIndex: 'ident',
},
{
title: t('资源名称'),
dataIndex: 'alias',
},
];
const handleSearchResource = (keyword: string) => {
setQuery(keyword);
};
const handleFetchList = (list: Array<resourceItem>) => {
const idents = list.map((item) => item.ident);
setSelectRowKeys(idents);
onSelect && onSelect(idents);
};
return (
<div>
<div className='table-search-area'>
<SearchInput
placeholder={t('资源信息或标签')}
onSearch={handleSearchResource}
></SearchInput>
</div>
<BaseTable
columns={columns}
rowKey={'ident'}
fetchParams={{
id: currentGroup?.id || '',
query,
}}
fetchHandle={getResourceList}
rowSelection={{
selectedRowKeys: selectRowKeys,
onChange: (selectedRowKeys: React.Key[]) => {
setSelectRowKeys(selectedRowKeys);
onSelect && onSelect(selectedRowKeys as Array<string>);
},
}}
paginationProps={{
simple: true,
}}
onFetchList={handleFetchList}
></BaseTable>
</div>
);
}
Example #9
Source File: index.tsx From fe-v5 with Apache License 2.0 | 4 votes |
AddUser: React.FC<TeamProps> = (props: TeamProps) => {
const { t } = useTranslation();
const { teamId, onSelect } = props;
const [teamInfo, setTeamInfo] = useState<Team>();
const [selectedUser, setSelectedUser] = useState<React.Key[]>([]);
const [selectedUserRows, setSelectedUserRows] = useState<User[]>([]);
const [query, setQuery] = useState('');
const userColumn: ColumnsType<User> = [
{
title: t('用户名'),
dataIndex: 'username',
ellipsis: true,
},
{
title: t('显示名'),
dataIndex: 'nickname',
ellipsis: true,
render: (text: string, record) => record.nickname || '-',
},
{
title: t('邮箱'),
dataIndex: 'email',
render: (text: string, record) => record.email || '-',
},
{
title: t('手机'),
dataIndex: 'phone',
render: (text: string, record) => record.phone || '-',
},
];
useEffect(() => {
getTeam();
}, []);
const getTeam = () => {
if (!teamId) return;
getTeamInfo(teamId).then((data) => {
setTeamInfo(data.user_group);
});
};
const handleClose = (val) => {
let newList = selectedUserRows.filter((item) => item.id !== val.id);
let newId = newList.map((item) => item.id);
setSelectedUserRows(newList);
setSelectedUser(newId);
};
const onSelectChange = (newKeys: [], newRows: []) => {
onSelect(newKeys);
setSelectedUser(newKeys);
setSelectedUserRows(newRows);
};
return (
<div>
<div>
<span>{t('团队名称')}:</span>
{teamInfo && teamInfo.name}
</div>
<div
style={{
margin: '20px 0 16px',
}}
>
{selectedUser.length > 0 && (
<span>
{t('已选择')}
{selectedUser.length}
{t('项')}:
</span>
)}
{selectedUserRows.map((item, index) => {
return (
<Tag
style={{
marginBottom: '4px',
}}
closable
onClose={() => handleClose(item)}
key={item.id}
>
{item.username}
</Tag>
);
})}
</div>
<Input
className={'searchInput'}
prefix={<SearchOutlined />}
placeholder={t('用户名、邮箱或电话')}
onPressEnter={(e) => {
setQuery((e.target as HTMLInputElement).value);
}}
/>
<BaseTable
fetchHandle={getUserInfoList}
columns={userColumn}
rowKey='id'
needPagination={true}
pageSize={5}
fetchParams={{
query,
}}
rowSelection={{
preserveSelectedRowKeys: true,
selectedRowKeys: selectedUser,
onChange: onSelectChange,
}}
></BaseTable>
</div>
);
}
Example #10
Source File: Wallets.tsx From subscan-multisig-react with Apache License 2.0 | 4 votes |
export function Wallets() {
const { t } = useTranslation();
const history = useHistory();
const { api, chain, network } = useApi();
const [multisigAccounts, setMultisigAccounts] = useState<KeyringAddress[]>([]);
const isExtensionAccount = useIsInjected();
const [isCalculating, setIsCalculating] = useState<boolean>(true);
const [searchKeyword, setSearchKeyword] = useState('');
const displayMultisigAccounts = useMemo(() => {
if (!searchKeyword.trim()) {
return multisigAccounts;
}
return multisigAccounts.filter(
(account) =>
(account.meta.name && account.meta.name.indexOf(searchKeyword.trim()) >= 0) ||
account.address.indexOf(searchKeyword.trim()) >= 0
);
}, [multisigAccounts, searchKeyword]);
const { linkColor, mainColor } = useMemo(() => {
return { linkColor: getLinkColor(network), mainColor: getThemeColor(network) };
}, [network]);
const exportAllWallets = () => {
if (!multisigAccounts || multisigAccounts.length < 1) {
return;
}
const configs: MultisigAccountConfig[] = [];
multisigAccounts.forEach((multisigAccount) => {
const config = {
name: multisigAccount.meta.name || '',
members: multisigAccount.meta.addressPair as { name: string; address: string }[],
threshold: multisigAccount.meta.threshold as number,
scope: getMultiAccountScope(multisigAccount.publicKey),
};
configs.push(config);
});
const blob = new Blob([JSON.stringify(configs)], { type: 'text/plain;charset=utf-8' });
saveAs(blob, `multisig_accounts.json`);
};
const uploadProps = {
name: 'file',
headers: {
authorization: 'authorization-text',
},
onChange(info: any) {
if (info.file.status !== 'uploading') {
// console.log(info.file, info.fileList);
}
// if (info.file.status === 'done') {
// message.success(`${info.file.name} file uploaded successfully`);
// } else if (info.file.status === 'error') {
// message.error(`${info.file.name} file upload failed.`);
// }
},
customRequest(info: any) {
try {
const reader = new FileReader();
reader.onload = (e: any) => {
// eslint-disable-next-line no-console
// console.log(e.target.result);
try {
const configs = JSON.parse(e.target.result) as MultisigAccountConfig[];
configs
.filter((config) => {
const encodeMembers = config.members.map((member) => {
return {
name: member.name,
address: encodeAddress(member.address, Number(chain.ss58Format)),
};
});
const publicKey = createKeyMulti(
encodeMembers.map((item) => item.address),
config.threshold
);
const acc = findMultiAccountFromKey(publicKey);
return !acc;
})
.forEach((config) => {
const encodeMembers = config.members.map((member) => {
return {
name: member.name,
address: encodeAddress(member.address, Number(chain.ss58Format)),
};
});
const signatories = encodeMembers.map(({ address }) => address);
const addressPair = config.members.map(({ address, ...other }) => ({
...other,
address: encodeAddress(address, Number(chain.ss58Format)),
}));
keyring.addMultisig(signatories, config.threshold, {
name: config.name,
addressPair,
genesisHash: api?.genesisHash.toHex(),
});
const publicKey = createKeyMulti(
encodeMembers.map((item) => item.address),
config.threshold
);
updateMultiAccountScopeFromKey(publicKey, ShareScope.all, [], network);
});
message.success(t('success'));
refreshMultisigAccounts();
} catch {
message.error(t('account config error'));
}
};
reader.readAsText(info.file);
} catch (err: unknown) {
if (err instanceof Error) {
// eslint-disable-next-line no-console
console.log('err:', err);
}
}
},
};
const renderAddress = (address: string) => (
<Link to={Path.extrinsic + '/' + address + history.location.hash} style={{ color: linkColor }}>
{address}
</Link>
);
const renderAction = useCallback(
(row: KeyringAddress) => {
// const { address } = row;
return (
<Space size="middle">
<div className="flex items-center">
<Button
type="primary"
className="flex items-center justify-center h-7"
onClick={() => {
history.push(Path.extrinsic + '/' + row.address + history.location.hash);
}}
style={{
borderRadius: '4px',
}}
>
{t('actions')}
</Button>
{(row as unknown as any).entries && (row as unknown as any).entries.length > 0 && (
<div className="ml-2 bg-red-500 rounded-full w-3 h-3"></div>
)}
</div>
</Space>
);
},
[history, t]
);
const columns: ColumnsType<KeyringAddress> = [
{
title: t('name'),
dataIndex: ['meta', 'name'],
},
{
title: t('address'),
dataIndex: 'address',
render: renderAddress,
},
{
title: t('balance'),
key: 'balance',
render: (account) => {
return <Space direction="vertical">{renderBalances(account, chain)}</Space>;
},
},
{
title: t('status.index'),
key: 'status',
render: (_, record) => {
const {
meta: { addressPair },
} = record;
return (addressPair as AddressPair[])?.some((item) => isExtensionAccount(item.address))
? t('available')
: t('watch only');
},
},
{
// title: t('actions'),
title: '',
key: 'action',
render: (_1: unknown, row) => renderAction(row),
},
];
const expandedRowRender = (record: KeyringAddress) => {
const columnsNested: ColumnsType<KeyringJson> = [
{ dataIndex: 'name' },
{
dataIndex: 'address',
render: (address) => (
<Space size="middle">
<BaseIdentityIcon theme="polkadot" size={32} value={address} />
<SubscanLink address={address} />
</Space>
),
},
{
key: 'isInject',
render: (_, pair) => t(isExtensionAccount(pair.address) ? 'injected' : 'external'),
},
];
return (
<div className="multisig-list-expand bg-gray-100 p-5 members">
<Table
columns={columnsNested}
dataSource={record.meta.addressPair as KeyringJson[]}
pagination={false}
bordered
rowKey="address"
className=" table-without-head"
/>
</div>
);
};
const refreshMultisigAccounts = useCallback(async () => {
setIsCalculating(true);
const accounts = keyring
.getAccounts()
.filter((account) => account.meta.isMultisig && isInCurrentScope(account.publicKey, network));
const balances = await api?.query.system.account.multi(accounts.map(({ address }) => address));
const entries = await Promise.all(
accounts.map(async ({ address }) => await api?.query.multisig.multisigs.entries(address))
);
setMultisigAccounts(
accounts.map((item, index) => {
(item.meta.addressPair as KeyringJson[]).forEach((key) => {
key.address = convertToSS58(key.address, Number(chain.ss58Format));
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const source = (balances as any)[index] as unknown as any;
return {
...item,
value: source.data.free.toString(),
kton: source.data.freeKton?.toString() ?? '0',
entries: entries[index] || [],
};
})
);
setIsCalculating(false);
}, [api, network, chain]);
useEffect(() => {
refreshMultisigAccounts();
}, [refreshMultisigAccounts]);
const menu = (
<Menu>
<Menu.Item key="1" onClick={exportAllWallets}>
{t('export all')}
</Menu.Item>
<Upload {...uploadProps} showUploadList={false}>
<Menu.Item key="2">{t('import all')}</Menu.Item>
</Upload>
</Menu>
);
if (!isCalculating && multisigAccounts.length === 0) {
return (
<Space
direction="vertical"
className="w-full h-full flex flex-col items-center justify-center absolute"
id="wallets"
>
<div className="flex flex-col items-center">
<AddIcon className="w-24 h-24" />
<div className="text-black-800 font-semibold text-xl lg:mt-16 lg:mb-10 mt-6 mb-4">
Please create Multisig wallet first
</div>
<Link to={Path.wallet + history.location.hash}>
<Button type="primary" className="w-48">
{t('wallet.add')}
</Button>
</Link>
<div className="my-1">{t('or')}</div>
<Upload {...uploadProps} showUploadList={false}>
<Button type="primary" className="w-48">
{t('import all')}
</Button>
</Upload>
</div>
</Space>
);
}
return (
<Space direction="vertical" className="absolute top-4 bottom-4 left-4 right-4 overflow-auto" id="wallets">
<div className="flex flex-col md:justify-between md:flex-row">
<div className="flex items-center">
<Link to={Path.wallet + history.location.hash}>
<Button type="primary" className="w-44">
{t('wallet.add')}
</Button>
</Link>
<Dropdown overlay={menu} trigger={['click']} placement="bottomCenter">
<MoreOutlined
className="ml-4 rounded-full opacity-60 cursor-pointer p-1"
style={{
color: mainColor,
backgroundColor: mainColor + '40',
}}
onClick={(e) => e.preventDefault()}
/>
</Dropdown>
{/* {multisigAccounts && multisigAccounts.length >= 1 && (
<Tooltip title={t('export all')}>
<ExportIcon className="ml-5 mt-1 w-8 h-8 cursor-pointer" onClick={exportAllWallets} />
</Tooltip>
)}
<Tooltip title={t('import all')}>
<Upload {...uploadProps} showUploadList={false}>
<ImportIcon className="ml-5 mt-1 w-8 h-8 cursor-pointer" />
</Upload>
</Tooltip> */}
</div>
<div className="w-56 mt-4 md:mt-0 md:w-72">
<Input
placeholder={t('wallet search placeholder')}
value={searchKeyword}
onChange={(e) => {
setSearchKeyword(e.target.value);
}}
/>
</div>
</div>
<Table
columns={columns}
dataSource={displayMultisigAccounts}
rowKey="address"
expandable={{ expandedRowRender, expandIcon: genExpandMembersIcon(t('members')), expandIconColumnIndex: 4 }}
pagination={false}
loading={isCalculating}
className="lg:block hidden multisig-list-table"
/>
<Space direction="vertical" className="lg:hidden block">
{displayMultisigAccounts.map((account) => {
const { address, meta } = account;
return (
<Collapse
key={address}
expandIcon={() => <></>}
collapsible={
(meta.addressPair as AddressPair[])?.some((item) => isExtensionAccount(item.address))
? 'header'
: 'disabled'
}
className="wallet-collapse"
>
<Panel
header={
<Space direction="vertical" className="w-full">
<Typography.Text className="mr-4">{meta.name}</Typography.Text>
<Typography.Text copyable>{address}</Typography.Text>
</Space>
}
key={address}
extra={
<Space direction="vertical" className="text-right">
<Space>{renderBalances(account, chain)}</Space>
{renderAction(account)}
</Space>
}
className="overflow-hidden mb-4"
>
<MemberList data={account} />
</Panel>
</Collapse>
);
})}
</Space>
</Space>
);
}
Example #11
Source File: groups.tsx From fe-v5 with Apache License 2.0 | 4 votes |
Resource: React.FC = () => { const { t } = useTranslation(); const { type } = useParams<{ type: string; }>(); const [activeKey, setActiveKey] = useState<UserType>(UserType.Team); const [visible, setVisible] = useState<boolean>(false); const [action, setAction] = useState<ActionType>(); const [userId, setUserId] = useState<string>(''); const [teamId, setTeamId] = useState<string>(''); const [memberId, setMemberId] = useState<string>(''); const [memberList, setMemberList] = useState<User[]>([]); const [allMemberList, setAllMemberList] = useState<User[]>([]); const [teamInfo, setTeamInfo] = useState<Team>(); const [teamList, setTeamList] = useState<Team[]>([]); const [memberLoading, setMemberLoading] = useState<boolean>(false); const [searchValue, setSearchValue] = useState<string>(''); const [searchMemberValue, setSearchMemberValue] = useState<string>(''); const userRef = useRef(null as any); let { profile } = useSelector<RootState, accountStoreState>((state) => state.account); const userColumn: ColumnsType<User> = [ { title: t('用户名'), dataIndex: 'username', ellipsis: true, }, { title: t('显示名'), dataIndex: 'nickname', ellipsis: true, render: (text: string, record) => record.nickname || '-', }, { title: t('邮箱'), dataIndex: 'email', render: (text: string, record) => record.email || '-', }, { title: t('手机'), dataIndex: 'phone', render: (text: string, record) => record.phone || '-', }, ]; const teamMemberColumns: ColumnsType<User> = [ ...userColumn, { title: t('操作'), width: '100px', render: ( text: string, record, // <DelPopover ) => ( // teamId={teamId} // memberId={record.id} // userType='member' // onClose={() => handleClose()} // ></DelPopover> <a style={{ color: 'red', }} onClick={() => { let params = { ids: [record.id], }; confirm({ title: t('是否删除该成员'), onOk: () => { deleteMember(teamId, params).then((_) => { message.success(t('成员删除成功')); handleClose('updateMember'); }); }, onCancel: () => {}, }); }} > {t('删除')} </a> ), }, ]; useEffect(() => { getList(true); }, []); //teamId变化触发 useEffect(() => { if (teamId) { getTeamInfoDetail(teamId); } }, [teamId]); const getList = (isDeleteOrAdd = false) => { getTeamList('', isDeleteOrAdd); }; // 获取团队列表 const getTeamList = (search?: string, isDelete?: boolean) => { getTeamInfoList({ query: search || '' }).then((data) => { setTeamList(data.dat || []); if ((!teamId || isDelete) && data.dat.length > 0) { setTeamId(data.dat[0].id); } }); }; // 获取团队详情 const getTeamInfoDetail = (id: string) => { setMemberLoading(true); getTeamInfo(id).then((data: TeamInfo) => { setTeamInfo(data.user_group); setMemberList(data.users); setAllMemberList(data.users); setMemberLoading(false); }); }; const handleSearch = (type?: string, val?: string) => { if (type === 'team') { getTeamList(val); } else { if (!val) { getTeamInfoDetail(teamId); } else { setMemberLoading(true); let newList = allMemberList.filter( (item) => item.username.indexOf(val) !== -1 || item.nickname.indexOf(val) !== -1 || item.id.toString().indexOf(val) !== -1 || item.phone.indexOf(val) !== -1 || item.email.indexOf(val) !== -1, ); setMemberList(newList); setMemberLoading(false); } } }; const handleClick = (type: ActionType, id?: string, memberId?: string) => { if (id) { setTeamId(id); } else { setTeamId(''); } if (memberId) { setMemberId(memberId); } else { setMemberId(''); } setAction(type); setVisible(true); }; // 弹窗关闭回调 const handleClose = (isDeleteOrAdd: boolean | string = false) => { setVisible(false); if (searchValue) { handleSearch('team', searchValue); } else { // 添加、删除成员 不用获取列表 if (isDeleteOrAdd !== 'updateMember') { getList(isDeleteOrAdd !== 'updateName'); // 修改名字,不用选中第一个 } } if (teamId && (isDeleteOrAdd === 'update' || isDeleteOrAdd === 'updateMember' || isDeleteOrAdd === 'updateName')) { getTeamInfoDetail(teamId); } }; return ( <PageLayout title={t('团队管理')} icon={<UserOutlined />} hideCluster> <div className='user-manage-content'> <div style={{ display: 'flex', height: '100%' }}> <div className='left-tree-area'> <div className='sub-title'> {t('团队列表')} <Button style={{ height: '30px', }} size='small' type='link' onClick={() => { handleClick(ActionType.CreateTeam); }} > {t('新建团队')} </Button> </div> <div style={{ display: 'flex', margin: '5px 0px 12px' }}> <Input prefix={<SearchOutlined />} value={searchValue} onChange={(e) => { setSearchValue(e.target.value); }} placeholder={t('搜索团队名称')} onPressEnter={(e) => { // @ts-ignore getTeamList(e.target.value); }} onBlur={(e) => { // @ts-ignore getTeamList(e.target.value); }} /> </div> <List style={{ marginBottom: '12px', flex: 1, overflow: 'auto', }} dataSource={teamList} size='small' renderItem={(item) => ( <List.Item key={item.id} className={teamId === item.id ? 'is-active' : ''} onClick={() => setTeamId(item.id)}> {item.name} </List.Item> )} /> </div> {teamList.length > 0 ? ( <div className='resource-table-content'> <Row className='team-info'> <Col span='24' style={{ color: '#000', fontSize: '14px', fontWeight: 'bold', display: 'inline', }} > {teamInfo && teamInfo.name} <EditOutlined title={t('刷新')} style={{ marginLeft: '8px', fontSize: '14px', }} onClick={() => handleClick(ActionType.EditTeam, teamId)} ></EditOutlined> <DeleteOutlined style={{ marginLeft: '8px', fontSize: '14px', }} onClick={() => { confirm({ title: t('是否删除该团队'), onOk: () => { deleteTeam(teamId).then((_) => { message.success(t('团队删除成功')); handleClose(true); }); }, onCancel: () => {}, }); }} /> </Col> <Col style={{ marginTop: '8px', color: '#666', }} > {t('备注')}:{teamInfo && teamInfo.note ? teamInfo.note : '-'} </Col> </Row> <Row justify='space-between' align='middle'> <Col span='12'> <Input prefix={<SearchOutlined />} value={searchMemberValue} className={'searchInput'} onChange={(e) => setSearchMemberValue(e.target.value)} placeholder={t('用户名、显示名、邮箱或手机')} onPressEnter={(e) => handleSearch('member', searchMemberValue)} /> </Col> <Button type='primary' ghost onClick={() => { handleClick(ActionType.AddUser, teamId); }} > {t('添加成员')} </Button> </Row> <Table rowKey='id' columns={teamMemberColumns} dataSource={memberList} loading={memberLoading} /> </div> ) : ( <div className='blank-busi-holder'> <p style={{ textAlign: 'left', fontWeight: 'bold' }}> <InfoCircleOutlined style={{ color: '#1473ff' }} /> {t('提示信息')} </p> <p> 没有与您相关的团队,请先 <a onClick={() => handleClick(ActionType.CreateTeam)}>创建团队</a> </p> </div> )} </div> <UserInfoModal visible={visible} action={action as ActionType} width={500} userType={activeKey} onClose={handleClose} onSearch={(val) => { setSearchValue(val); handleSearch('team', val); }} userId={userId} teamId={teamId} memberId={memberId} /> </div> </PageLayout> ); }
Example #12
Source File: users.tsx From fe-v5 with Apache License 2.0 | 4 votes |
Resource: React.FC = () => {
const { t } = useTranslation();
const [activeKey, setActiveKey] = useState<UserType>(UserType.User);
const [visible, setVisible] = useState<boolean>(false);
const [action, setAction] = useState<ActionType>();
const [userId, setUserId] = useState<string>('');
const [teamId, setTeamId] = useState<string>('');
const [memberId, setMemberId] = useState<string>('');
const [allMemberList, setAllMemberList] = useState<User[]>([]);
const [teamList, setTeamList] = useState<Team[]>([]);
const [query, setQuery] = useState<string>('');
const [searchValue, setSearchValue] = useState<string>('');
const userRef = useRef(null as any);
let { profile } = useSelector<RootState, accountStoreState>((state) => state.account);
const userColumn: ColumnsType<User> = [
{
title: t('用户名'),
dataIndex: 'username',
ellipsis: true,
},
{
title: t('显示名'),
dataIndex: 'nickname',
ellipsis: true,
render: (text: string, record) => record.nickname || '-',
},
{
title: t('邮箱'),
dataIndex: 'email',
render: (text: string, record) => record.email || '-',
},
{
title: t('手机'),
dataIndex: 'phone',
render: (text: string, record) => record.phone || '-',
},
];
const userColumns: ColumnsType<User> = [
...userColumn,
{
title: t('角色'),
dataIndex: 'roles',
render: (text: [], record) => text.join(', '),
},
{
title: t('操作'),
width: '240px',
render: (text: string, record) => (
<>
<Button className='oper-name' type='link' onClick={() => handleClick(ActionType.EditUser, record.id)}>
{t('编辑')}
</Button>
<Button className='oper-name' type='link' onClick={() => handleClick(ActionType.Reset, record.id)}>
{t('重置密码')}
</Button>
{/* <DelPopover
userId={record.id}
userType='user'
onClose={() => handleClose()}
></DelPopover> */}
<a
style={{
color: 'red',
marginLeft: '16px',
}}
onClick={() => {
confirm({
title: t('是否删除该用户'),
onOk: () => {
deleteUser(record.id).then((_) => {
message.success(t('用户删除成功'));
handleClose();
});
},
onCancel: () => {},
});
}}
>
{t('删除')}
</a>
</>
),
},
];
if (!profile.roles.includes('Admin')) {
userColumns.pop(); //普通用户不展示操作列
}
const getList = () => {
userRef.current.refreshList();
};
const handleClick = (type: ActionType, id?: string, memberId?: string) => {
if (id) {
activeKey === UserType.User ? setUserId(id) : setTeamId(id);
} else {
activeKey === UserType.User ? setUserId('') : setTeamId('');
}
if (memberId) {
setMemberId(memberId);
} else {
setMemberId('');
}
setAction(type);
setVisible(true);
};
// 弹窗关闭回调
const handleClose = () => {
setVisible(false);
getList();
};
const onSearchQuery = (e) => {
let val = e.target.value;
setQuery(val);
};
return (
<PageLayout title={t('用户管理')} icon={<UserOutlined />} hideCluster>
<div className='user-manage-content'>
<div className='user-content'>
<Row className='event-table-search'>
<div className='event-table-search-left'>
<Input className={'searchInput'} prefix={<SearchOutlined />} onPressEnter={onSearchQuery} placeholder={t('用户名、邮箱或手机')} />
</div>
<div className='event-table-search-right'>
{activeKey === UserType.User && profile.roles.includes('Admin') && (
<div className='user-manage-operate'>
<Button type='primary' onClick={() => handleClick(activeKey === UserType.User ? ActionType.CreateUser : t('创建团队'))} ghost>
{t('创建用户')}
</Button>
</div>
)}
</div>
</Row>
<BaseTable
ref={userRef}
fetchHandle={getUserInfoList}
columns={userColumns}
rowKey='id'
needPagination={true}
fetchParams={{
query,
}}
></BaseTable>
</div>
<UserInfoModal
visible={visible}
action={action as ActionType}
width={activeKey === UserType.User ? 500 : 700}
userType={activeKey}
onClose={handleClose}
userId={userId}
teamId={teamId}
memberId={memberId}
/>
</div>
</PageLayout>
);
}
Example #13
Source File: index.tsx From fe-v5 with Apache License 2.0 | 4 votes |
Shield: React.FC = () => {
const { t } = useTranslation();
const history = useHistory();
const dispatch = useDispatch();
const [query, setQuery] = useState<string>('');
const { curBusiItem } = useSelector<RootState, CommonStoreState>((state) => state.common);
const [bgid, setBgid] = useState(undefined);
const [clusters, setClusters] = useState<string[]>([]);
const [currentShieldDataAll, setCurrentShieldDataAll] = useState<Array<shieldItem>>([]);
const [currentShieldData, setCurrentShieldData] = useState<Array<shieldItem>>([]);
const [loading, setLoading] = useState<boolean>(false);
const columns: ColumnsType = [
{
title: t('集群'),
dataIndex: 'cluster',
render: (data) => {
return <div>{data}</div>;
},
},
{
title: t('标签'),
dataIndex: 'tags',
render: (text: any) => {
return (
<>
{text
? text.map((tag, index) => {
return tag ? (
// <ColorTag text={`${tag.key} ${tag.func} ${tag.func === 'in' ? tag.value.split(' ').join(', ') : tag.value}`} key={index}>
// </ColorTag>
<div key={index} style={{ lineHeight: '16px' }}>{`${tag.key} ${tag.func} ${tag.func === 'in' ? tag.value.split(' ').join(', ') : tag.value}`}</div>
) : null;
})
: ''}
</>
);
},
},
{
title: t('屏蔽原因'),
dataIndex: 'cause',
render: (text: string, record: shieldItem) => {
return (
<>
<Tooltip placement='topLeft' title={text}>
<div
style={{
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
lineHeight: '16px',
}}
>
{text}
</div>
</Tooltip>
by {record.create_by}
</>
);
},
},
{
title: t('屏蔽时间'),
dataIndex: 'btime',
render: (text: number, record: shieldItem) => {
return (
<div className='shield-time'>
<div>
{t('开始:')}
{dayjs(record?.btime * 1000).format('YYYY-MM-DD HH:mm:ss')}
</div>
<div>
{t('结束:')}
{dayjs(record?.etime * 1000).format('YYYY-MM-DD HH:mm:ss')}
</div>
</div>
);
},
},
// {
// title: t('创建人'),
// ellipsis: true,
// dataIndex: 'create_by',
// },
{
title: t('操作'),
width: '98px',
dataIndex: 'operation',
render: (text: undefined, record: shieldItem) => {
return (
<>
<div className='table-operator-area'>
<div
className='table-operator-area-normal'
style={{
cursor: 'pointer',
display: 'inline-block',
}}
onClick={() => {
dispatch({
type: 'shield/setCurShieldData',
data: record,
});
curBusiItem?.id && history.push(`/alert-mutes/edit/${record.id}?mode=clone`);
}}
>
{t('克隆')}
</div>
<div
className='table-operator-area-warning'
style={{
cursor: 'pointer',
display: 'inline-block',
}}
onClick={() => {
confirm({
title: t('确定删除该告警屏蔽?'),
icon: <ExclamationCircleOutlined />,
onOk: () => {
dismiss(record.id);
},
onCancel() {},
});
}}
>
{t('删除')}
</div>
</div>
</>
);
},
},
];
useEffect(() => {
getList();
}, [curBusiItem]);
useEffect(() => {
filterData();
}, [query, clusters, currentShieldDataAll]);
const dismiss = (id: number) => {
deleteShields({ ids: [id] }, curBusiItem.id).then((res) => {
refreshList();
if (res.err) {
message.success(res.err);
} else {
message.success(t('删除成功'));
}
});
};
const filterData = () => {
const data = JSON.parse(JSON.stringify(currentShieldDataAll));
const res = data.filter((item: shieldItem) => {
const tagFind = item.tags.find((tag) => {
return tag.key.indexOf(query) > -1 || tag.value.indexOf(query) > -1 || tag.func.indexOf(query) > -1;
});
return (item.cause.indexOf(query) > -1 || !!tagFind) && ((clusters && clusters?.indexOf(item.cluster) > -1) || clusters?.length === 0);
});
setCurrentShieldData(res || []);
};
const getList = async () => {
if (curBusiItem.id) {
setLoading(true);
const { success, dat } = await getShieldList({ id: curBusiItem.id });
if (success) {
setCurrentShieldDataAll(dat || []);
setLoading(false);
}
}
};
const refreshList = () => {
getList();
};
const onSearchQuery = (e) => {
let val = e.target.value;
setQuery(val);
};
const clusterChange = (data) => {
setClusters(data);
};
const busiChange = (data) => {
setBgid(data);
};
return (
<PageLayout title={t('屏蔽规则')} icon={<CloseCircleOutlined />} hideCluster>
<div className='shield-content'>
<LeftTree
busiGroup={{
// showNotGroupItem: true,
onChange: busiChange,
}}
></LeftTree>
{curBusiItem?.id ? (
<div className='shield-index'>
<div className='header'>
<div className='header-left'>
<RefreshIcon
className='strategy-table-search-left-refresh'
onClick={() => {
refreshList();
}}
/>
<ColumnSelect onClusterChange={(e) => setClusters(e)} />
<Input onPressEnter={onSearchQuery} className={'searchInput'} prefix={<SearchOutlined />} placeholder={t('搜索标签、屏蔽原因')} />
</div>
<div className='header-right'>
<Button
type='primary'
className='add'
ghost
onClick={() => {
history.push('/alert-mutes/add');
}}
>
{t('新增屏蔽规则')}
</Button>
</div>
</div>
<Table
rowKey='id'
// sticky
pagination={{
total: currentShieldData.length,
showQuickJumper: true,
showSizeChanger: true,
showTotal: (total) => {
return `共 ${total} 条数据`;
},
pageSizeOptions: pageSizeOptionsDefault,
defaultPageSize: 30,
}}
loading={loading}
dataSource={currentShieldData}
columns={columns}
/>
</div>
) : (
<BlankBusinessPlaceholder text='屏蔽规则' />
)}
</div>
</PageLayout>
);
}
Example #14
Source File: index.tsx From fe-v5 with Apache License 2.0 | 4 votes |
Shield: React.FC = () => {
const { t } = useTranslation();
const history = useHistory();
const [query, setQuery] = useState<string>('');
const { curBusiItem } = useSelector<RootState, CommonStoreState>((state) => state.common);
const [bgid, setBgid] = useState(undefined);
const [clusters, setClusters] = useState<string[]>([]);
const [currentShieldDataAll, setCurrentShieldDataAll] = useState<Array<subscribeItem>>([]);
const [currentShieldData, setCurrentShieldData] = useState<Array<subscribeItem>>([]);
const [loading, setLoading] = useState<boolean>(false);
const columns: ColumnsType = [
{
title: t('集群'),
dataIndex: 'cluster',
render: (data) => {
return <div>{data}</div>;
},
},
{
title: t('告警规则'),
dataIndex: 'rule_name',
render: (data) => {
return <div>{data}</div>;
},
},
{
title: t('订阅标签'),
dataIndex: 'tags',
render: (text: any) => {
return (
<>
{text
? text.map((tag, index) => {
return tag ? <div key={index}>{`${tag.key} ${tag.func} ${tag.func === 'in' ? tag.value.split(' ').join(', ') : tag.value}`}</div> : null;
})
: ''}
</>
);
},
},
{
title: t('告警接收组'),
dataIndex: 'user_groups',
render: (text: string, record: subscribeItem) => {
return (
<>
{record.user_groups?.map((item) => (
<ColorTag text={item.name} key={item.id}></ColorTag>
))}
</>
);
},
},
{
title: t('编辑人'),
ellipsis: true,
dataIndex: 'update_by',
},
{
title: t('操作'),
width: '128px',
dataIndex: 'operation',
render: (text: undefined, record: subscribeItem) => {
return (
<>
<div className='table-operator-area'>
<div
className='table-operator-area-normal'
style={{
cursor: 'pointer',
display: 'inline-block',
}}
onClick={() => {
curBusiItem?.id && history.push(`/alert-subscribes/edit/${record.id}`);
}}
>
{t('编辑')}
</div>
<div
className='table-operator-area-normal'
style={{
cursor: 'pointer',
display: 'inline-block',
}}
onClick={() => {
curBusiItem?.id && history.push(`/alert-subscribes/edit/${record.id}?mode=clone`);
}}
>
{t('克隆')}
</div>
<div
className='table-operator-area-warning'
style={{
cursor: 'pointer',
display: 'inline-block',
}}
onClick={() => {
confirm({
title: t('确定删除该订阅规则?'),
icon: <ExclamationCircleOutlined />,
onOk: () => {
dismiss(record.id);
},
onCancel() {},
});
}}
>
{t('删除')}
</div>
</div>
</>
);
},
},
];
useEffect(() => {
getList();
}, [curBusiItem]);
useEffect(() => {
filterData();
}, [query, clusters, currentShieldDataAll]);
const dismiss = (id: number) => {
deleteSubscribes({ ids: [id] }, curBusiItem.id).then((res) => {
refreshList();
if (res.err) {
message.success(res.err);
} else {
message.success(t('删除成功'));
}
});
};
const filterData = () => {
const data = JSON.parse(JSON.stringify(currentShieldDataAll));
const res = data.filter((item: subscribeItem) => {
const tagFind = item?.tags?.find((tag) => {
return tag.key.indexOf(query) > -1 || tag.value.indexOf(query) > -1 || tag.func.indexOf(query) > -1;
});
const groupFind = item?.user_groups?.find((item) => {
return item?.name?.indexOf(query) > -1;
});
return (item?.rule_name?.indexOf(query) > -1 || !!tagFind || !!groupFind) && ((clusters && clusters?.indexOf(item.cluster) > -1) || clusters?.length === 0);
});
setCurrentShieldData(res || []);
};
const getList = async () => {
if (curBusiItem.id) {
setLoading(true);
const { success, dat } = await getSubscribeList({ id: curBusiItem.id });
if (success) {
setCurrentShieldDataAll(dat || []);
setLoading(false);
}
}
};
const refreshList = () => {
getList();
};
const onSearchQuery = (e) => {
let val = e.target.value;
setQuery(val);
};
const clusterChange = (data) => {
setClusters(data);
};
const busiChange = (data) => {
setBgid(data);
};
return (
<PageLayout title={t('订阅规则')} icon={<CopyOutlined />} hideCluster>
<div className='shield-content'>
<LeftTree
busiGroup={{
// showNotGroupItem: true,
onChange: busiChange,
}}
></LeftTree>
{curBusiItem?.id ? (
<div className='shield-index'>
<div className='header'>
<div className='header-left'>
<RefreshIcon
className='strategy-table-search-left-refresh'
onClick={() => {
refreshList();
}}
/>
<ColumnSelect onClusterChange={(e) => setClusters(e)} />
<Input onPressEnter={onSearchQuery} className={'searchInput'} prefix={<SearchOutlined />} placeholder={t('搜索规则、标签、接收组')} />
</div>
<div className='header-right'>
<Button
type='primary'
className='add'
ghost
onClick={() => {
history.push('/alert-subscribes/add');
}}
>
{t('新增订阅规则')}
</Button>
</div>
</div>
<Table
rowKey='id'
pagination={{
total: currentShieldData.length,
showQuickJumper: true,
showSizeChanger: true,
showTotal: (total) => {
return `共 ${total} 条数据`;
},
pageSizeOptions: pageSizeOptionsDefault,
defaultPageSize: 30,
}}
loading={loading}
dataSource={currentShieldData}
columns={columns}
/>
</div>
) : (
<BlankBusinessPlaceholder text='订阅规则' />
)}
</div>
</PageLayout>
);
}
Example #15
Source File: ConditionalStyle.tsx From datart with Apache License 2.0 | 4 votes |
ConditionalStyle: FC<ItemLayoutProps<ChartStyleConfig>> = memo( ({ ancestors, translate: t = title => title, data, onChange, dataConfigs, context, }) => { const [myData] = useState(() => CloneValueDeep(data)); const [visible, setVisible] = useState<boolean>(false); const [dataSource, setDataSource] = useState<ConditionalStyleFormValues[]>( myData.value || [], ); const [currentItem, setCurrentItem] = useState<ConditionalStyleFormValues>( {} as ConditionalStyleFormValues, ); const onEditItem = (values: ConditionalStyleFormValues) => { setCurrentItem(CloneValueDeep(values)); openConditionalStyle(); }; const onRemoveItem = (values: ConditionalStyleFormValues) => { const result: ConditionalStyleFormValues[] = dataSource.filter( item => item.uid !== values.uid, ); setDataSource(result); onChange?.(ancestors, { ...myData, value: result, }); }; const tableColumnsSettings: ColumnsType<ConditionalStyleFormValues> = [ { title: t('conditionalStyleTable.header.range.title'), dataIndex: 'range', width: 100, render: (_, { range }) => ( <Tag>{t(`conditionalStyleTable.header.range.${range}`)}</Tag> ), }, { title: t('conditionalStyleTable.header.operator'), dataIndex: 'operator', }, { title: t('conditionalStyleTable.header.value'), dataIndex: 'value', render: (_, { value }) => <>{JSON.stringify(value)}</>, }, { title: t('conditionalStyleTable.header.color.title'), dataIndex: 'value', render: (_, { color }) => ( <> <Tag color={color.background}> {t('conditionalStyleTable.header.color.background')} </Tag> <Tag color={color.textColor}> {t('conditionalStyleTable.header.color.text')} </Tag> </> ), }, { title: t('conditionalStyleTable.header.action'), dataIndex: 'action', width: 140, render: (_, record) => { return [ <Button type="link" key="edit" onClick={() => onEditItem(record)}> {t('conditionalStyleTable.btn.edit')} </Button>, <Popconfirm key="remove" placement="topRight" title={t('conditionalStyleTable.btn.confirm')} onConfirm={() => onRemoveItem(record)} > <Button type="link" danger> {t('conditionalStyleTable.btn.remove')} </Button> </Popconfirm>, ]; }, }, ]; const openConditionalStyle = () => { setVisible(true); }; const closeConditionalStyleModal = () => { setVisible(false); setCurrentItem({} as ConditionalStyleFormValues); }; const submitConditionalStyleModal = ( values: ConditionalStyleFormValues, ) => { let result: ConditionalStyleFormValues[] = []; if (values.uid) { result = dataSource.map(item => { if (item.uid === values.uid) { return values; } return item; }); } else { result = [...dataSource, { ...values, uid: uuidv4() }]; } setDataSource(result); closeConditionalStyleModal(); onChange?.(ancestors, { ...myData, value: result, }); }; return ( <StyledConditionalStylePanel direction="vertical"> <Button type="primary" onClick={openConditionalStyle}> {t('conditionalStyleTable.btn.add')} </Button> <Row gutter={24}> <Col span={24}> <Table<ConditionalStyleFormValues> bordered={true} size="small" pagination={false} rowKey={record => record.uid!} columns={tableColumnsSettings} dataSource={dataSource} /> </Col> </Row> <AddModal context={context} visible={visible} translate={t} values={currentItem} onOk={submitConditionalStyleModal} onCancel={closeConditionalStyleModal} /> </StyledConditionalStylePanel> ); }, itemLayoutComparer, )
Example #16
Source File: ScorecardConditionalStyle.tsx From datart with Apache License 2.0 | 4 votes |
ScorecardConditionalStyle: FC<ItemLayoutProps<ChartStyleConfig>> = memo( ({ ancestors, translate: t = title => title, data, onChange, dataConfigs, context, }) => { const [myData] = useState(() => CloneValueDeep(data)); const [allItems] = useState(() => { let results: ChartStyleSelectorItem[] = []; try { results = typeof myData?.options?.getItems === 'function' ? myData?.options?.getItems.call( null, dataConfigs?.map(col => AssignDeep(col)), ) || [] : []; } catch (error) { console.error(`ListTemplatePanel | invoke action error ---> `, error); } return results; }); const [visible, setVisible] = useState<boolean>(false); const [dataSource, setDataSource] = useState< ScorecardConditionalStyleFormValues[] >( myData?.value?.filter(item => allItems.find(ac => ac.key === item.metricKey), ) || [], ); const [currentItem, setCurrentItem] = useState<ScorecardConditionalStyleFormValues>( {} as ScorecardConditionalStyleFormValues, ); const onEditItem = (values: ScorecardConditionalStyleFormValues) => { setCurrentItem(CloneValueDeep(values)); openConditionalStyle(); }; const onRemoveItem = (values: ScorecardConditionalStyleFormValues) => { const result: ScorecardConditionalStyleFormValues[] = dataSource.filter( item => item.uid !== values.uid, ); setDataSource(result); onChange?.(ancestors, { ...myData, value: result, }); }; const tableColumnsSettings: ColumnsType<ScorecardConditionalStyleFormValues> = [ { title: t('viz.palette.data.metrics', true), dataIndex: 'metricKey', render: key => { return allItems.find(v => v.key === key)?.label; }, }, { title: t('conditionalStyleTable.header.operator'), dataIndex: 'operator', }, { title: t('conditionalStyleTable.header.value'), dataIndex: 'value', render: (_, { value }) => <>{JSON.stringify(value)}</>, }, { title: t('conditionalStyleTable.header.color.title'), dataIndex: 'value', render: (_, { color }) => ( <> <Tag color={color.textColor}> {t('conditionalStyleTable.header.color.text')} </Tag> <Tag color={color.background}> {t('conditionalStyleTable.header.color.background')} </Tag> </> ), }, { title: t('conditionalStyleTable.header.action'), dataIndex: 'action', width: 140, render: (_, record) => { return [ <Button type="link" key="edit" onClick={() => onEditItem(record)}> {t('conditionalStyleTable.btn.edit')} </Button>, <Popconfirm key="remove" placement="topRight" title={t('conditionalStyleTable.btn.confirm')} onConfirm={() => onRemoveItem(record)} > <Button type="link" danger> {t('conditionalStyleTable.btn.remove')} </Button> </Popconfirm>, ]; }, }, ]; const openConditionalStyle = () => { setVisible(true); }; const closeConditionalStyleModal = () => { setVisible(false); setCurrentItem({} as ScorecardConditionalStyleFormValues); }; const submitConditionalStyleModal = ( values: ScorecardConditionalStyleFormValues, ) => { let result: ScorecardConditionalStyleFormValues[] = []; if (values.uid) { result = dataSource.map(item => { if (item.uid === values.uid) { return values; } return item; }); } else { result = [...dataSource, { ...values, uid: uuidv4() }]; } setDataSource(result); closeConditionalStyleModal(); onChange?.(ancestors, { ...myData, value: result, }); }; return ( <StyledScorecardConditionalStylePanel direction="vertical"> <Button type="primary" onClick={openConditionalStyle}> {t('conditionalStyleTable.btn.add')} </Button> <Row gutter={24}> <Col span={24}> <Table<ScorecardConditionalStyleFormValues> bordered={true} size="small" pagination={false} rowKey={record => record.uid!} columns={tableColumnsSettings} dataSource={dataSource} /> </Col> </Row> <AddModal context={context} visible={visible} translate={t} values={currentItem} allItems={allItems} onOk={submitConditionalStyleModal} onCancel={closeConditionalStyleModal} /> </StyledScorecardConditionalStylePanel> ); }, itemLayoutComparer, )
Example #17
Source File: Entries.tsx From subscan-multisig-react with Apache License 2.0 | 4 votes |
// eslint-disable-next-line complexity
export function Entries({
source,
isConfirmed,
isCancelled,
account,
loading,
totalCount,
currentPage,
onChangePage,
}: EntriesProps) {
const { t } = useTranslation();
const isInjected = useIsInjected();
const { network } = useApi();
const renderAction = useCallback(
// eslint-disable-next-line complexity
(row: Entry) => {
if (row.status) {
return <span>{t(`status.${row.status}`)}</span>;
}
const actions: TxActionType[] = [];
// eslint-disable-next-line react/prop-types
const pairs = (account.meta?.addressPair ?? []) as AddressPair[];
const injectedAccounts: string[] = pairs.filter((pair) => isInjected(pair.address)).map((pair) => pair.address);
if (injectedAccounts.includes(row.depositor)) {
actions.push('cancel');
}
const localAccountInMultisigPairList = intersection(
injectedAccounts,
pairs.map((pair) => pair.address)
);
const approvedLocalAccounts = intersection(localAccountInMultisigPairList, row.approvals);
if (approvedLocalAccounts.length !== localAccountInMultisigPairList.length) {
actions.push('approve');
}
if (actions.length === 0) {
// eslint-disable-next-line react/prop-types
if (row.approvals && row.approvals.length === account.meta.threshold) {
actions.push('pending');
}
}
return (
<Space>
{actions.map((action) => {
if (action === 'pending') {
return (
<Button key={action} disabled>
{t(action)}
</Button>
);
} else if (action === 'approve') {
return <TxApprove key={action} entry={row} />;
} else {
return <TxCancel key={action} entry={row} />;
}
})}
</Space>
);
},
[account.meta?.addressPair, account.meta.threshold, isInjected, t]
);
const columns: ColumnsType<Entry> = [
{
title: t(isConfirmed || isCancelled ? 'extrinsic_index' : 'call_data'),
dataIndex: isConfirmed || isCancelled ? 'extrinsicIdx' : 'hexCallData',
width: 300,
align: 'left',
// eslint-disable-next-line complexity
render(data: string) {
let extrinsicHeight = '';
let extrinsicIndex = '';
if ((isConfirmed || isCancelled) && data.split('-').length > 1) {
extrinsicHeight = data.split('-')[0];
extrinsicIndex = data.split('-')[1];
}
return !(isConfirmed || isCancelled) ? (
<>
<Typography.Text copyable={!isEmpty(data) && { text: data }}>
{!isEmpty(data)
? // ? `${data.substring(0, CALL_DATA_LENGTH)}${data.length > CALL_DATA_LENGTH ? '...' : ''}`
toShortString(data, CALL_DATA_LENGTH)
: '-'}
</Typography.Text>
</>
) : (
<SubscanLink extrinsic={{ height: extrinsicHeight, index: extrinsicIndex }}>{data}</SubscanLink>
);
},
},
{
title: t('actions'),
dataIndex: 'callDataJson',
align: 'left',
render: renderMethod,
},
{
title: t('progress'),
dataIndex: 'approvals',
align: 'left',
render(approvals: string[]) {
const cur = (approvals && approvals.length) || 0;
return cur + '/' + account.meta.threshold;
},
},
{
title: t('status.index'),
key: 'status',
align: 'left',
render: (_, row) => renderAction(row),
},
];
const expandedRowRender = (entry: Entry) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const progressColumnsNested: ColumnsType<any> = [
{ dataIndex: 'name', width: 100 },
{
width: 400,
dataIndex: 'address',
render: (address) => (
<Space size="middle">
<BaseIdentityIcon theme="polkadot" size={32} value={address} />
<SubscanLink address={address} copyable />
</Space>
),
},
{
width: 250,
key: 'status',
render: (_, pair) => renderMemberStatus(entry, pair, network, !isCancelled && !isConfirmed),
},
];
// const callDataJson = entry.callData?.toJSON() ?? {};
const args: Required<ArgObj>[] = ((entry.meta?.args ?? []) as Required<ArgObj>[]).map((arg) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const value = (entry.callDataJson?.args as any)[arg?.name ?? ''];
return { ...arg, value };
});
return (
<div className="record-expand bg-gray-100 py-3 px-5">
<div className=" text-black-800 text-base leading-none mb-3">{t('progress')}</div>
<div className="members">
<Table
columns={progressColumnsNested}
dataSource={account.meta.addressPair as { key: string; name: string; address: string }[]}
pagination={false}
bordered
rowKey="address"
showHeader={false}
className="mb-4 mx-4"
/>
</div>
<div className=" text-black-800 text-base leading-none my-3">{t('parameters')}</div>
<Args
args={args}
className="mb-4 mx-4"
section={entry.callDataJson?.section}
method={entry.callDataJson?.method}
/>
</div>
);
};
return (
<div className="record-table">
<Table
loading={loading}
dataSource={source}
columns={columns}
rowKey={(record) => record.callHash ?? (record.blockHash as string)}
pagination={
isConfirmed || isCancelled
? {
total: totalCount,
pageSize: 10,
current: currentPage,
onChange: onChangePage,
}
: false
}
expandable={{
expandedRowRender,
expandIcon: genExpandIcon(),
expandIconColumnIndex: 4,
}}
className="lg:block hidden"
></Table>
<Space direction="vertical" className="lg:hidden block">
{source.map((data) => {
const { address, hash, callData, approvals } = data;
const approvedCount = approvals.length || 0;
const threshold = (account.meta.threshold as number) || 1;
return (
<Collapse key={address} expandIcon={() => <></>} className="wallet-collapse">
<Panel
header={
<Space direction="vertical" className="w-full mb-4">
<Typography.Text className="mr-4" copyable>
{hash}
</Typography.Text>
<div className="flex items-center">
<Typography.Text>{renderMethod(callData)}</Typography.Text>
<Progress
/* eslint-disable-next-line no-magic-numbers */
percent={parseInt(String((approvedCount / threshold) * 100), 10)}
steps={threshold}
className="ml-4"
/>
</div>
</Space>
}
key={address}
extra={renderAction(data)}
className="overflow-hidden mb-4"
>
<MemberList
data={account}
statusRender={(pair) => renderMemberStatus(data, pair, network, !isCancelled && !isConfirmed)}
/>
</Panel>
</Collapse>
);
})}
{!source.length && <Empty />}
</Space>
</div>
);
}
Example #18
Source File: access.tsx From shippo with MIT License | 4 votes |
Page_permission_access: React.FC = () => {
const [data, setData] = useState<IPermissionAccess[]>()
const editAccessDrawerRef = useRef<EditAccessDrawerRef>(null)
const handleDle = useCallback((id: number) => {
confirm({
title: '确认删除?',
icon: <ExclamationCircleOutlined />,
content: '此操作不可逆',
onOk() {
console.log('OK')
services.permissionAccess.del({ id }).then((hr) => {
if (hr.data.success) {
message.success('成功')
} else {
message.success('失败')
}
})
},
onCancel() {
console.log('Cancel')
},
})
}, [])
const [columns, setColumns] = useState<ColumnsType<IPermissionAccess>>([
{
title: '访问规则ID',
dataIndex: 'id',
key: 'id',
},
{
title: '访问规则表达式',
dataIndex: 'accessRule',
key: 'accessRule',
},
{
title: '描述',
dataIndex: 'remark',
key: 'remark',
},
{
title: '访问规则类型',
dataIndex: 'accessType',
key: 'accessType',
},
{
title: '被引用次数',
dataIndex: 'permissionAssociationCount',
key: 'permissionAssociationCount',
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Space size="middle">
<Button
type="link"
onClick={() => {
editAccessDrawerRef.current?.open(record)
}}
>
修改
</Button>
<Button
type="link"
onClick={() => {
handleDle(record.id)
}}
>
删除
</Button>
</Space>
),
},
])
const updateTable = useCallback(async () => {
const hr = await services.permissionAccess.find_all()
setData(
hr.data.resource.map((item) => {
return { ...item, createdAt: formatTimeStr(item.createdAt) }
})
)
}, [])
useMount(() => {
updateTable()
})
return (
<div>
<EditAccessDrawer ref={editAccessDrawerRef} onClose={() => updateTable()} />
<Space size="middle">
<Button type="primary" onClick={() => editAccessDrawerRef.current?.open()}>
新增访问规则
</Button>
</Space>
<Table
rowKey="id"
columns={columns}
dataSource={data}
pagination={{ position: ['bottomCenter'] }}
size="small"
/>
</div>
)
}
Example #19
Source File: business.tsx From fe-v5 with Apache License 2.0 | 4 votes |
Resource: React.FC = () => {
const { t } = useTranslation();
const urlQuery = useQuery();
const id = urlQuery.get('id');
const [visible, setVisible] = useState<boolean>(false);
const [action, setAction] = useState<ActionType>();
const [teamId, setTeamId] = useState<string>(id || '');
const [memberId, setMemberId] = useState<string>('');
const [memberList, setMemberList] = useState<{ user_group: any }[]>([]);
const [memberTotal, setMemberTotal] = useState<number>(0);
const [allMemberList, setAllMemberList] = useState<User[]>([]);
const [teamInfo, setTeamInfo] = useState<{ name: string; id: number }>();
const [teamList, setTeamList] = useState<Team[]>([]);
const [query, setQuery] = useState<string>('');
const [memberLoading, setMemberLoading] = useState<boolean>(false);
const [searchValue, setSearchValue] = useState<string>('');
const [searchMemberValue, setSearchMemberValue] = useState<string>('');
const userRef = useRef(null as any);
let { profile } = useSelector<RootState, accountStoreState>((state) => state.account);
const dispatch = useDispatch();
const teamMemberColumns: ColumnsType<any> = [
{
title: t('团队名称'),
dataIndex: ['user_group', 'name'],
ellipsis: true,
},
{
title: t('团队备注'),
dataIndex: ['user_group', 'note'],
ellipsis: true,
render: (text: string, record) => record['user_group'].note || '-',
},
{
title: t('权限'),
dataIndex: 'perm_flag',
},
{
title: t('操作'),
width: '100px',
render: (text: string, record) => (
<a
style={{
color: memberList.length > 1 ? 'red' : '#00000040',
}}
onClick={() => {
if (memberList.length <= 1) return;
let params = [
{
user_group_id: record['user_group'].id,
busi_group_id: teamId,
},
];
confirm({
title: t('是否删除该团队'),
onOk: () => {
deleteBusinessTeamMember(teamId, params).then((_) => {
message.success(t('团队删除成功'));
handleClose('deleteMember');
});
},
onCancel: () => {},
});
}}
>
{t('删除')}
</a>
),
},
];
useEffect(() => {
teamId && getTeamInfoDetail(teamId);
}, [teamId]);
useEffect(() => {
getTeamList();
}, []);
const getList = (action) => {
getTeamList(undefined, action === 'delete');
};
// 获取业务组列表
const getTeamList = (search?: string, isDelete?: boolean) => {
let params = {
query: search === undefined ? searchValue : search,
limit: PAGE_SIZE,
};
getBusinessTeamList(params).then((data) => {
setTeamList(data.dat || []);
if ((!teamId || isDelete) && data.dat.length > 0) {
setTeamId(data.dat[0].id);
}
});
};
// 获取业务组详情
const getTeamInfoDetail = (id: string) => {
setMemberLoading(true);
getBusinessTeamInfo(id).then((data) => {
dispatch({
type: 'common/saveData',
prop: 'curBusiItem',
data: data,
});
setTeamInfo(data);
setMemberList(data.user_groups);
setMemberLoading(false);
});
};
const handleClick = (type: ActionType, id?: string, memberId?: string) => {
if (memberId) {
setMemberId(memberId);
} else {
setMemberId('');
}
setAction(type);
setVisible(true);
};
// 弹窗关闭回调
const handleClose = (action) => {
setVisible(false);
if (['create', 'delete', 'update'].includes(action)) {
getList(action);
dispatch({ type: 'common/getBusiGroups' });
}
if (teamId && ['update', 'addMember', 'deleteMember'].includes(action)) {
getTeamInfoDetail(teamId);
}
};
return (
<PageLayout title={t('业务组管理')} icon={<UserOutlined />} hideCluster>
<div className='user-manage-content'>
<div style={{ display: 'flex', height: '100%' }}>
<div className='left-tree-area'>
<div className='sub-title'>
{t('业务组列表')}
<Button
style={{
height: '30px',
}}
size='small'
type='link'
onClick={() => {
handleClick(ActionType.CreateBusiness);
}}
>
{t('新建业务组')}
</Button>
</div>
<div style={{ display: 'flex', margin: '5px 0px 12px' }}>
<Input
prefix={<SearchOutlined />}
placeholder={t('搜索业务组名称')}
onPressEnter={(e) => {
// @ts-ignore
getTeamList(e.target.value);
}}
onBlur={(e) => {
// @ts-ignore
getTeamList(e.target.value);
}}
/>
</div>
<List
style={{
marginBottom: '12px',
flex: 1,
overflow: 'auto',
}}
dataSource={teamList}
size='small'
renderItem={(item) => (
<List.Item key={item.id} className={teamId == item.id ? 'is-active' : ''} onClick={() => setTeamId(item.id)}>
{item.name}
</List.Item>
)}
/>
</div>
{teamList.length > 0 ? (
<div className='resource-table-content'>
<Row className='team-info'>
<Col
span='24'
style={{
color: '#000',
fontSize: '14px',
fontWeight: 'bold',
display: 'inline',
}}
>
{teamInfo && teamInfo.name}
<EditOutlined
title={t('刷新')}
style={{
marginLeft: '8px',
fontSize: '14px',
}}
onClick={() => handleClick(ActionType.EditBusiness, teamId)}
></EditOutlined>
<DeleteOutlined
style={{
marginLeft: '8px',
fontSize: '14px',
}}
onClick={() => {
confirm({
title: t('是否删除该业务组'),
onOk: () => {
deleteBusinessTeam(teamId).then((_) => {
message.success(t('业务组删除成功'));
handleClose('delete');
});
},
onCancel: () => {},
});
}}
/>
</Col>
<Col
style={{
marginTop: '8px',
color: '#666',
}}
>
{t('备注')}:{t('告警规则,告警事件,监控对象,自愈脚本等都归属业务组,是一个在系统里可以自闭环的组织')}
</Col>
</Row>
<Row justify='space-between' align='middle'>
<Col span='12'>
<Input
prefix={<SearchOutlined />}
value={searchMemberValue}
className={'searchInput'}
onChange={(e) => setSearchMemberValue(e.target.value)}
placeholder={t('搜索团队名称')}
/>
</Col>
<Button
type='primary'
ghost
onClick={() => {
handleClick(ActionType.AddBusinessMember, teamId);
}}
>
{t('添加团队')}
</Button>
</Row>
<Table
rowKey='id'
columns={teamMemberColumns}
dataSource={memberList && memberList.length > 0 ? memberList.filter((item) => item.user_group && item.user_group.name.indexOf(searchMemberValue) !== -1) : []}
loading={memberLoading}
/>
</div>
) : (
<div className='blank-busi-holder'>
<p style={{ textAlign: 'left', fontWeight: 'bold' }}>
<InfoCircleOutlined style={{ color: '#1473ff' }} /> {t('提示信息')}
</p>
<p>
业务组(监控对象、监控大盘、告警规则、自愈脚本都要归属某个业务组)为空,请先
<a onClick={() => handleClick(ActionType.CreateBusiness)}>创建业务组</a>
</p>
</div>
)}
</div>
</div>
<UserInfoModal
visible={visible}
action={action as ActionType}
userType={'business'}
onClose={handleClose}
teamId={teamId}
onSearch={(val) => {
setTeamId(val);
}}
/>
</PageLayout>
);
}
Example #20
Source File: index.tsx From fe-v5 with Apache License 2.0 | 4 votes |
export default function Dashboard() {
const { t } = useTranslation();
const [form] = Form.useForm();
const ref = useRef(null);
const history = useHistory();
const [isModalVisible, setIsModalVisible] = useState(false);
const [modalType, setModalType] = useState<ModalStatus>(ModalStatus.None);
const [selectRowKeys, setSelectRowKeys] = useState<number[]>([]);
const [exportData, setExportData] = useState<string>('');
const [editing, setEditing] = useState(false);
const [query, setQuery] = useState<string>('');
const [searchVal, setsearchVal] = useState<string>('');
const [dashboardList, setDashboardList] = useState<DashboardType[]>();
const [busiId, setBusiId] = useState<number>();
const showModal = () => {
setIsModalVisible(true);
};
const handleOk = async () => {
await form.validateFields();
if (editing) {
await edit();
message.success(t('编辑大盘成功'));
} else {
await create();
message.success(t('新建大盘成功'));
}
(ref?.current as any)?.refreshList();
setIsModalVisible(false);
setEditing(false);
};
useEffect(() => {
if (busiId) {
getDashboard(busiId).then((res) => {
if (searchVal && res.dat) {
const filters = searchVal.split(' ');
for (var i = 0; i < filters.length; i++) {
res.dat = res.dat.filter((item) => item.name.includes(filters[i]) || item.tags.includes(filters[i]));
}
}
setDashboardList(res.dat);
});
}
}, [busiId, searchVal]);
const create = async () => {
let { name, tags } = form.getFieldsValue();
return (
busiId &&
createDashboard(busiId, {
name,
tags,
})
);
};
const edit = async () => {
let { name, tags, id } = form.getFieldsValue();
return (
busiId &&
updateSingleDashboard(busiId, id, {
name,
tags,
pure: true,
})
);
};
const handleEdit = (record: DashboardType) => {
const { id, name, tags } = record;
form.setFieldsValue({
name,
tags,
id,
});
setIsModalVisible(true);
setEditing(true);
};
const handleTagClick = (tag) => {
const queryItem = query.length > 0 ? query.split(' ') : [];
if (queryItem.includes(tag)) return;
setQuery((query) => query + ' ' + tag);
setsearchVal((searchVal) => searchVal + ' ' + tag);
};
const layout = {
labelCol: {
span: 4,
},
wrapperCol: {
span: 16,
},
};
const dashboardColumn: ColumnsType = [
{
title: t('大盘名称'),
dataIndex: 'name',
render: (text: string, record: DashboardType) => {
const { t } = useTranslation();
return (
<div className='table-active-text' onClick={() => history.push(`/dashboard/${busiId}/${record.id}`)}>
{text}
</div>
);
},
},
{
title: t('分类标签'),
dataIndex: 'tags',
render: (text: string[]) => (
<>
{text.map((tag, index) => {
return tag ? (
<Tag
color='blue'
key={index}
style={{
cursor: 'pointer',
}}
onClick={() => handleTagClick(tag)}
>
{tag}
</Tag>
) : null;
})}
</>
),
},
{
title: t('更新时间'),
dataIndex: 'update_at',
render: (text: number) => dayjs(text * 1000).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: t('发布人'),
dataIndex: 'create_by',
},
{
title: t('操作'),
width: '240px',
render: (text: string, record: DashboardType) => (
<div className='table-operator-area'>
<div className='table-operator-area-normal' onClick={() => handleEdit(record)}>
{t('编辑')}
</div>
<div
className='table-operator-area-normal'
onClick={async () => {
confirm({
title: `${t('是否克隆大盘')}${record.name}?`,
onOk: async () => {
await cloneDashboard(busiId as number, record.id);
message.success(t('克隆大盘成功'));
(ref?.current as any)?.refreshList();
},
onCancel() {},
});
}}
>
{t('克隆')}
</div>
<div
className='table-operator-area-warning'
onClick={async () => {
confirm({
title: `${t('是否删除大盘')}:${record.name}?`,
onOk: async () => {
await removeDashboard(busiId as number, record.id);
message.success(t('删除大盘成功'));
(ref?.current as any)?.refreshList();
},
onCancel() {},
});
}}
>
{t('删除')}
</div>
</div>
),
},
];
const onSearchQuery = (e) => {
let val = e.target.value;
setsearchVal(val);
};
const handleImportDashboard = async (data) => {
const { dat } = await importDashboard(busiId as number, data);
return dat || {};
};
return (
<PageLayout title={t('监控大盘')} icon={<FundViewOutlined />} hideCluster={true}>
<div style={{ display: 'flex' }}>
<LeftTree busiGroup={{ onChange: (id) => setBusiId(id) }}></LeftTree>
{busiId ? (
<div className='dashboard' style={{ flex: 1, overflow: 'auto' }}>
<div className='table-handle'>
<div className='table-handle-search'>
<Input
onPressEnter={onSearchQuery}
className={'searchInput'}
value={query}
onChange={(e) => setQuery(e.target.value)}
prefix={<SearchOutlined />}
placeholder={t('大盘名称、分类标签')}
/>
</div>
<div className='table-handle-buttons'>
<Button type='primary' onClick={showModal} ghost>
{t('新建大盘')}
</Button>
<div className={'table-more-options'}>
<Dropdown
overlay={
<ul className='ant-dropdown-menu'>
<li className='ant-dropdown-menu-item' onClick={() => setModalType(ModalStatus.BuiltIn)}>
<span>{t('导入监控大盘')}</span>
</li>
<li
className='ant-dropdown-menu-item'
onClick={async () => {
if (selectRowKeys.length) {
let exportData = await exportDashboard(busiId as number, selectRowKeys);
setExportData(JSON.stringify(exportData.dat, null, 2));
setModalType(ModalStatus.Export);
} else {
message.warning(t('未选择任何大盘'));
}
}}
>
<span>{t('导出监控大盘')}</span>
</li>
<li
className='ant-dropdown-menu-item'
onClick={() => {
if (selectRowKeys.length) {
confirm({
title: '是否批量删除大盘?',
onOk: async () => {
const reuqests = selectRowKeys.map((id) => {
console.log(id);
return removeDashboard(busiId as number, id);
});
Promise.all(reuqests).then(() => {
message.success(t('批量删除大盘成功'));
});
// TODO: 删除完后立马刷新数据有时候不是实时的,这里暂时间隔0.5s后再刷新列表
setTimeout(() => {
(ref?.current as any)?.refreshList();
}, 500);
},
onCancel() {},
});
} else {
message.warning(t('未选择任何大盘'));
}
}}
>
<span>{t('批量删除大盘')}</span>
</li>
</ul>
}
trigger={['click']}
>
<Button onClick={(e) => e.stopPropagation()}>
{t('更多操作')}
<DownOutlined
style={{
marginLeft: 2,
}}
/>
</Button>
</Dropdown>
</div>
</div>
</div>
<Table
dataSource={dashboardList}
className='dashboard-table'
columns={dashboardColumn}
pagination={{
total: dashboardList?.length,
showTotal(total: number) {
return `共 ${total} 条数据`;
},
pageSizeOptions: [30, 50, 100, 300],
defaultPageSize: 30,
showSizeChanger: true,
}}
rowKey='id'
rowSelection={{
selectedRowKeys: selectRowKeys,
onChange: (selectedRowKeys: number[]) => {
setSelectRowKeys(selectedRowKeys);
},
}}
></Table>
</div>
) : (
<BlankBusinessPlaceholder text='监控大盘' />
)}
</div>
<Modal
title={editing ? t('编辑监控大盘') : t('创建新监控大盘')}
visible={isModalVisible}
onOk={handleOk}
onCancel={() => {
setIsModalVisible(false);
}}
destroyOnClose
>
<Form {...layout} form={form} preserve={false}>
<Form.Item
label={t('大盘名称')}
name='name'
wrapperCol={{
span: 24,
}}
rules={[
{
required: true,
message: t('请输入大盘名称'),
},
]}
>
<Input />
</Form.Item>
<Form.Item
wrapperCol={{
span: 24,
}}
label={t('分类标签')}
name='tags'
>
<Select
mode='tags'
dropdownStyle={{
display: 'none',
}}
placeholder={t('请输入分类标签(请用回车分割)')}
></Select>
</Form.Item>
<Form.Item name='id' hidden>
<Input />
</Form.Item>
</Form>
</Modal>
<ImportAndDownloadModal
bgid={busiId}
status={modalType}
crossCluster={false}
fetchBuiltinFunc={getBuiltinDashboards}
submitBuiltinFunc={createBuiltinDashboards}
onClose={() => {
setModalType(ModalStatus.None);
}}
onSuccess={() => {
(ref?.current as any)?.refreshList();
}}
onSubmit={handleImportDashboard}
label='大盘'
title={
ModalStatus.Export === modalType ? (
'大盘'
) : (
<Tabs defaultActiveKey={ModalStatus.BuiltIn} onChange={(e: ModalStatus) => setModalType(e)} className='custom-import-alert-title'>
<TabPane tab=' 导入内置大盘模块' key={ModalStatus.BuiltIn}></TabPane>
<TabPane tab='导入大盘JSON' key={ModalStatus.Import}></TabPane>
</Tabs>
)
}
exportData={exportData}
/>
</PageLayout>
);
}
Example #21
Source File: ObjectAttrStruct.tsx From next-basics with GNU General Public License v3.0 | 4 votes |
export function LegacyObjectAttrStructForm(
props: LegacyObjectAttrStructProps
): React.ReactElement {
const [value, setValue] = React.useState<Partial<StructValueType>>({
default: "",
struct_define: [],
});
const [addStructMode, setAddStructMode] = React.useState("new");
const [addStructModalVisible, setAddStructModalVisible] =
React.useState(false);
const [importStructModalVisible, setImportStructModalVisible] =
React.useState(false);
const [currentStruct, setCurrentStruct] = React.useState<StructDefine>(
{} as StructDefine
);
const [cmdbObjectList, setCmdbObjectList] = React.useState<
Partial<CmdbModels.ModelCmdbObject>[]
>([]);
const [selectedObjectId, setSelectedObjectId] = React.useState("");
const [curValueType, setCurValueType] = React.useState("");
const [selectedObjectAttrKeys, setSelectedObjectAttrKeys] = React.useState(
[]
);
const [loadingObject, setLoadingObject] = React.useState(false);
const memoizeAttrList = React.useMemo(() => {
if (selectedObjectId.length) {
return cmdbObjectList
.filter((object) => object.objectId === selectedObjectId)[0]
?.attrList.filter(
(attr) => !["struct", "structs"].includes(attr.value.type)
);
} else {
return [];
}
}, [cmdbObjectList, selectedObjectId]);
const handleValueChange = (value: Partial<StructValueType>) => {
setValue(value);
props.onChange && props.onChange(value);
};
const { getFieldDecorator } = props.form;
const handleDeleteStruct = (struct: StructDefine) => {
Modal.confirm({
title: i18n.t(`${NS_FORMS}:${K.NOTICE}`),
content: (
<>
{i18n.t(`${NS_FORMS}:${K.DELETE_STRUCTURE_ITEM_PREFIX}`)}
<Tag color="red">{struct.name}</Tag>
{i18n.t(`${NS_FORMS}:${K.DELETE_STRUCTURE_ITEM_POSTFIX}`)}
</>
),
onOk() {
const structs = value.struct_define.filter(
(item) => item.id !== struct.id
);
handleValueChange({ ...value, struct_define: structs });
},
});
};
const loadCmdbObjectList = async () => {
setCmdbObjectList((await CmdbObjectApi_getObjectAll({})).data);
setLoadingObject(false);
};
const getOptionBtns = (record: any): React.ReactNode => (
<div className="struct-option-btn-group" style={{ display: "flex" }}>
{addStructMode === "new" && (
<Button
type="link"
icon={<EditOutlined />}
onClick={(e) => {
setCurrentStruct(record);
setCurValueType(record.type);
setAddStructModalVisible(true);
}}
/>
)}
<Button
type="link"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
handleDeleteStruct(record);
}}
/>
</div>
);
const DragHandle = SortableHandle(() => (
<SwapOutlined className={styles.iconRotate} />
));
const SortableItem = SortableElement((props: any) => <tr {...props} />);
const SortableBody = SortableContainer((props: any) => <tbody {...props} />);
const onSortEnd = ({ oldIndex, newIndex }: SortEnd) => {
const dataSource = value?.struct_define;
if (oldIndex !== newIndex) {
const tempData = [].concat(dataSource);
const temp = tempData[oldIndex];
tempData[oldIndex] = tempData[newIndex];
tempData[newIndex] = temp;
handleValueChange({
...value,
struct_define: tempData.filter((el) => !!el),
});
}
};
const DraggableContainer = (props: any) => (
<SortableBody
useDragHandle
disableAutoscroll
helperClass={styles["row-dragging"]}
onSortEnd={onSortEnd}
{...props}
/>
);
const DraggableBodyRow = ({ className, style, ...restProps }: any) => {
const dataSource = value?.struct_define;
// function findIndex base on Table rowKey props and should always be a right array index
const index = dataSource.findIndex(
(x) => x?.id === restProps["data-row-key"]
);
return <SortableItem index={index} {...restProps} />;
};
const structColumns = [
{
title: i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_ID}`),
dataIndex: "id",
key: "id",
},
{
title: i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_NAME}`),
dataIndex: "name",
key: "name",
},
{
title: i18n.t(`${NS_FORMS}:${K.TYPE}`),
dataIndex: "type",
key: "type",
render: (text: string, record: any) =>
valueTypeList.filter((type) => type.key === text)[0].text,
},
{
title: i18n.t(`${NS_FORMS}:${K.HANDEL}`),
key: "action",
render: (text: string, record: any) => getOptionBtns(record),
},
{
title: "排序",
dataIndex: "sort",
width: 70,
className: styles["drag-visible"],
render: () => <DragHandle />,
},
];
const structWithEnumColumns = [
{
title: i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_ID}`),
dataIndex: "id",
key: "id",
},
{
title: i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_NAME}`),
dataIndex: "name",
key: "name",
},
{
title: i18n.t(`${NS_FORMS}:${K.TYPE}`),
dataIndex: "type",
key: "type",
render: (text: string, record: any) =>
valueTypeList.filter((type) => type.key === text)[0].text,
},
{
title: i18n.t(`${NS_FORMS}:${K.ENUM_REGEX_JSON}`),
dataIndex: "regex",
key: "regex",
render: (text: any, record: { regex: any[]; type: string }) => {
if (
Array.isArray(record.regex) &&
["enums", "enum"].includes(record.type)
) {
return record.regex.join(",") || "";
} else if (record.type === "ip") {
return IPRegex;
}
return record.regex || "";
},
},
{
title: i18n.t(`${NS_FORMS}:${K.HANDEL}`),
key: "action",
render: (text: any, record: any) => getOptionBtns(record),
},
{
title: "排序",
dataIndex: "sort",
width: 70,
className: styles["drag-visible"],
render: () => <DragHandle />,
},
];
React.useEffect(() => {
!isNil(props.value) && setValue(props.value);
}, [props.value]);
React.useEffect(() => {
if (addStructMode !== "new") {
loadCmdbObjectList();
}
}, [addStructMode]);
const handleModeChange = (e: RadioChangeEvent) => {
setAddStructMode(e.target.value);
handleValueChange({ ...value, struct_define: [] });
e.target.value !== "new" && setLoadingObject(true);
};
const handleAddStructConfirm = () => {
//istanbul ignore next
props.form.validateFields(async (err, data) => {
if (err) {
return;
}
const new_struct_define = [...value.struct_define];
if (isEmpty(currentStruct)) {
new_struct_define.push({ ...data, isNew: true });
} else {
const currentStructId = value.struct_define.findIndex(
(item) => item.id === currentStruct.id
);
if (currentStructId !== -1) {
new_struct_define[currentStructId] = {
...data,
isNew: new_struct_define[currentStructId].isNew,
};
}
}
handleValueChange({
...value,
struct_define: new_struct_define,
});
setAddStructModalVisible(false);
setCurValueType("");
props.form.resetFields();
});
};
const rowSelection = {
onChange: (
selectedRowKeys: string[] | number[],
selectedRows: StructDefine[]
) => {
setSelectedObjectAttrKeys(selectedRowKeys);
},
selectedRowKeys: selectedObjectAttrKeys,
};
const handleObjectChange = (e: string) => {
setSelectedObjectId(e);
setSelectedObjectAttrKeys(
cmdbObjectList
.filter((object) => object.objectId === e)[0]
?.attrList.filter(
(attr) => !["struct", "structs"].includes(attr.value.type)
)
.map((attr, index) => attr.id)
);
};
return (
<div>
{i18n.t(`${NS_FORMS}:${K.STRUCTURE_BODY_DEFINATION}`)}
<div>
<Row>
<Radio.Group value={addStructMode} onChange={handleModeChange}>
<Radio value="new">
{i18n.t(`${NS_FORMS}:${K.NEW_DEFINATION}`)}
</Radio>
<Radio value="import">{i18n.t(`${NS_FORMS}:${K.IFEM}`)}</Radio>
</Radio.Group>
</Row>
<Row style={{ marginTop: 8 }}>
{addStructMode === "new" ? (
<Button
icon={<PlusOutlined />}
onClick={() => {
setCurrentStruct({} as StructDefine);
setAddStructModalVisible(true);
}}
>
{i18n.t(`${NS_FORMS}:${K.ADD_STRUCTURE_ITEM}`)}
</Button>
) : (
<Button
icon={<PlusOutlined />}
onClick={() => {
setImportStructModalVisible(true);
}}
loading={loadingObject}
>
{i18n.t(`${NS_FORMS}:${K.SELECT_MODEL}`)}
</Button>
)}
</Row>
</div>
<div style={{ marginTop: 15 }}>
<Table
columns={
(value?.struct_define?.some((item) => regexType.includes(item.type))
? structWithEnumColumns
: structColumns) as ColumnsType<StructDefine>
}
dataSource={value?.struct_define}
pagination={false}
rowKey="id"
components={{
body: {
wrapper: DraggableContainer,
row: DraggableBodyRow,
},
}}
/>
</div>
<Modal
title={
isEmpty(currentStruct)
? i18n.t(`${NS_FORMS}:${K.TITLE_ADD_STRUCTURE_ITEM}`)
: i18n.t(`${NS_FORMS}:${K.TITLE_EDIT_STRUCTURE_ITEM}`)
}
visible={addStructModalVisible}
onOk={handleAddStructConfirm}
onCancel={() => {
setCurValueType("");
props.form.resetFields();
setAddStructModalVisible(false);
}}
>
<Form
labelCol={{ span: currentLang === "zh" ? 6 : 10 }}
wrapperCol={{ span: 16 }}
>
<Form.Item label={i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_ID}`)}>
{getFieldDecorator("id", {
initialValue: isEmpty(currentStruct) ? "" : currentStruct.id,
rules: [
{
required: true,
message: i18n.t(`${NS_FORMS}:${K.INPUT_STRUCTURE_ITEM_ID}`),
},
{
pattern: /^[a-zA-Z][a-zA-Z_0-9]{0,31}$/gi,
message: i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_ID_LIMIT}`),
},
{
validator: (rule, curValue, cb) => {
const structIdArr =
value.struct_define?.map((r) => r.id) || [];
if (
currentStruct.id !== curValue &&
structIdArr.includes(curValue)
) {
cb(
i18n.t(`${NS_FORMS}:${K.DUPLICATE_STRUCTURE_ITEM_ID}`, {
id: curValue,
})
);
}
cb();
},
},
],
})(<Input autoFocus />)}
</Form.Item>
<Form.Item label={i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_NAME}`)}>
{getFieldDecorator("name", {
initialValue: isEmpty(currentStruct) ? "" : currentStruct.name,
rules: [
{
required: true,
message: i18n.t(`${NS_FORMS}:${K.INPUT_STRUCTURE_ITEM_NAME}`),
},
],
})(<Input />)}
</Form.Item>
<Form.Item label={i18n.t(`${NS_FORMS}:${K.TYPE}`)}>
{getFieldDecorator("type", {
initialValue: isEmpty(currentStruct) ? "" : currentStruct.type,
rules: [
{
required: true,
message: i18n.t(`${NS_FORMS}:${K.ENTER_TYPE}`),
},
],
})(
<Select
onChange={(value) => setCurValueType(value as string)}
style={{ width: "100%" }}
disabled={!isEmpty(currentStruct) && !currentStruct?.isNew}
>
{valueTypeList
.filter(
(type) => type.key !== "struct" && type.key !== "structs"
)
.map((item) => (
<Option key={item.key} value={item.key}>
{item.text}
</Option>
))}
</Select>
)}
</Form.Item>
{(curValueType === "enum" || curValueType === "enums") && (
<Form.Item label={i18n.t(`${NS_FORMS}:${K.ENUMERATION_VALUE}`)}>
{getFieldDecorator("regex", {
initialValue:
isEmpty(currentStruct) || isNil(currentStruct.regex)
? []
: currentStruct.regex,
})(
<Select
mode="tags"
style={{ width: "100%" }}
placeholder={i18n.t(
`${NS_FORMS}:${K.PLEASE_INPUT_ENUMERATED_VALUE}`
)}
/>
)}
</Form.Item>
)}
{(curValueType === "str" ||
curValueType === "int" ||
curValueType === "arr" ||
curValueType === "json") && (
<Form.Item
label={
curValueType === "json"
? "JSON Schema:"
: i18n.t(`${NS_FORMS}:${K.REGULAR}`)
}
>
{getFieldDecorator("regex", {
initialValue: isEmpty(currentStruct) ? "" : currentStruct.regex,
})(
<Input
placeholder={i18n.t(`${NS_FORMS}:${K.THIS_IS_NOT_MANDATORY}`)}
/>
)}
</Form.Item>
)}
{curValueType === "ip" && (
<Form.Item label={i18n.t(`${NS_FORMS}:${K.REGULAR}`)}>
<Input.TextArea
value={IPRegex}
style={{ wordBreak: "break-all" }}
disabled={true}
autoSize={{ minRows: 4 }}
resize={false}
/>
</Form.Item>
)}
</Form>
</Modal>
<Modal
title={i18n.t(`${NS_FORMS}:${K.CITE_MODEL}`)}
visible={importStructModalVisible}
onOk={() => {
handleValueChange({
...value,
struct_define: selectedObjectAttrKeys.map((id) => {
const selectedRow = memoizeAttrList.filter(
(attr) => attr.id === id
)[0];
return {
id: selectedRow.id,
name: selectedRow.name,
type: selectedRow.value.type,
regex: selectedRow.value.regex,
};
}),
});
setSelectedObjectId("");
setImportStructModalVisible(false);
}}
onCancel={() => {
setSelectedObjectId("");
setImportStructModalVisible(false);
}}
width={800}
>
<Select
showSearch
placeholder={i18n.t(
`${NS_FORMS}:${K.SELECT_ONE_CMDB_RESOURCE_MODEL}`
)}
filterOption={(input, option) =>
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >=
0
}
onChange={handleObjectChange}
value={selectedObjectId}
style={{ width: "100%" }}
>
{cmdbObjectList.map(
(object: Partial<CmdbModels.ModelCmdbObject>, index) => (
<Option key={index} value={object.objectId}>
{object.name}
</Option>
)
)}
</Select>
{selectedObjectId.length > 0 && (
<div style={{ marginTop: 15 }}>
{i18n.t(`${NS_FORMS}:${K.SELECT_ATTRIBUTE}`)}
<Table
columns={objectAttrColumns}
dataSource={memoizeAttrList}
rowSelection={rowSelection as any}
pagination={false}
rowKey={"id"}
/>
</div>
)}
</Modal>
</div>
);
}
Example #22
Source File: DevicesPage.tsx From iot-center-v2 with MIT License | 4 votes |
DevicesPage: FunctionComponent<Props> = ({helpCollapsed}) => {
const [loading, setLoading] = useState(true)
const [message, setMessage] = useState<Message | undefined>(undefined)
const [data, setData] = useState(NO_DEVICES)
const [dataStamp, setDataStamp] = useState(0)
const [lastEntries, setLastEntries] = useState(NO_ENTRIES)
useEffect(() => {
setLoading(true)
const fetchDevices = async () => {
try {
const response = await fetch('/api/devices')
if (response.status >= 300) {
const text = await response.text()
throw new Error(`${response.status} ${text}`)
}
const data = (await response.json()) as Array<DeviceInfo>
setData(data)
setLastEntries(
await Promise.all(
data.map(({deviceId}) => fetchLastEntryTime(deviceId))
)
)
} catch (e) {
setMessage({
title: 'Cannot fetch data',
description: String(e),
type: 'error',
})
} finally {
setLoading(false)
}
}
fetchDevices()
}, [dataStamp])
const removeAuthorization = async (device: DeviceInfo) => {
try {
setLoading(true)
const response = await fetch(`/api/devices/${device.deviceId}`, {
method: 'DELETE',
})
if (response.status >= 300) {
const text = await response.text()
throw new Error(`${response.status} ${text}`)
}
setLoading(false)
antdMessage.success(`Device ${device.deviceId} was unregistered`, 2)
} catch (e) {
setLoading(false)
setMessage({
title: 'Cannot remove device',
description: String(e),
type: 'error',
})
} finally {
setDataStamp(dataStamp + 1)
}
}
const addAuthorization = async (deviceId: string, deviceType: string) => {
try {
setLoading(true)
const response = await fetch(`/api/env/${deviceId}`)
if (response.status >= 300) {
const text = await response.text()
throw new Error(`${response.status} ${text}`)
}
const {newlyRegistered} = await response.json()
if (newlyRegistered && deviceType !== '') {
const setTypeResponse = await fetch(
`/api/devices/${deviceId}/type/${deviceType}`,
{
method: 'POST',
}
)
if (setTypeResponse.status >= 300) {
const text = await setTypeResponse.text()
throw new Error(`${setTypeResponse.status} ${text}`)
}
}
setLoading(false)
if (newlyRegistered) {
antdMessage.success(`Device '${deviceId}' was registered`, 2)
} else {
antdMessage.success(`Device '${deviceId}' is already registered`, 2)
}
} catch (e) {
setLoading(false)
setMessage({
title: 'Cannot register device',
description: String(e),
type: 'error',
})
} finally {
setDataStamp(dataStamp + 1)
}
}
// define table columns
const columnDefinitions: ColumnsType<DeviceInfo> = [
{
title: 'Device ID',
dataIndex: 'deviceId',
defaultSortOrder: 'ascend',
render: (deviceId: string) => (
<Link to={`/devices/${deviceId}`}>{deviceId}</Link>
),
},
{
title: 'Registration Time',
dataIndex: 'createdAt',
responsive: helpCollapsed ? ['lg'] : ['xxl'],
},
{
title: 'Last Entry',
dataIndex: 'deviceId',
render: (id: string) => {
const lastEntry = lastEntries.find(
({deviceId}) => deviceId === id
)?.lastEntry
if (lastEntry != null && lastEntry !== 0)
return timeFormatter({
timeZone: 'UTC',
format: 'YYYY-MM-DD HH:mm:ss ZZ',
})(lastEntry)
},
responsive: helpCollapsed ? ['xl'] : [],
},
{
title: '',
key: 'action',
align: 'right',
render: (_: string, device: DeviceInfo) => (
<>
<Tooltip title="Go to device settings" placement="topRight">
<Button
type="text"
icon={<IconSettings />}
href={`/devices/${device.deviceId}`}
/>
</Tooltip>
<Tooltip title="Go to device dashboard" placement="topRight">
<Button
type="text"
icon={<IconDashboard />}
href={`/dashboard/${device.deviceId}`}
/>
</Tooltip>
<Popconfirm
icon={<ExclamationCircleFilled style={{color: 'red'}} />}
title={`Are you sure to remove '${device.deviceId}' ?`}
onConfirm={() => removeAuthorization(device)}
okText="Yes"
okType="danger"
cancelText="No"
>
<Tooltip title="Remove device" placement="topRight" color="red">
<Button type="text" icon={<IconDelete />} />
</Tooltip>
</Popconfirm>
</>
),
},
]
return (
<PageContent
title="Device Registrations"
spin={loading}
message={message}
titleExtra={
<>
<Tooltip title="Register a new Device">
<Button
onClick={() => {
let deviceId = ''
let deviceType = ''
Modal.confirm({
title: 'Register Device',
icon: '',
content: (
<Form
name="registerDevice"
initialValues={{deviceId, deviceType}}
>
<Form.Item
name="deviceId"
rules={[
{required: true, message: 'Please input device ID !'},
]}
>
<Input
placeholder="Device ID"
onChange={(e) => {
deviceId = e.target.value
}}
/>
<Input
placeholder="Device type"
onChange={(e) => {
deviceType = e.target.value
}}
/>
</Form.Item>
</Form>
),
onOk: () => {
addAuthorization(deviceId, deviceType)
},
okText: 'Register',
})
}}
>
Register
</Button>
</Tooltip>
<Tooltip title="Reload Table">
<Button
type="primary"
onClick={() => setDataStamp(dataStamp + 1)}
style={{marginRight: '8px'}}
>
Reload
</Button>
</Tooltip>
</>
}
>
<Table
dataSource={data}
columns={columnDefinitions}
rowKey={deviceTableRowKey}
/>
</PageContent>
)
}
Example #23
Source File: DevicePage.tsx From iot-center-v2 with MIT License | 4 votes |
DevicePage: FunctionComponent<
RouteComponentProps<PropsRoute> & Props
> = ({match, location, helpCollapsed, mqttEnabled}) => {
const deviceId = match.params.deviceId ?? VIRTUAL_DEVICE
const [loading, setLoading] = useState(true)
const [message, setMessage] = useState<Message | undefined>()
const [deviceData, setDeviceData] = useState<DeviceData | undefined>()
const [dataStamp, setDataStamp] = useState(0)
const [progress, setProgress] = useState(-1)
const writeAllowed =
deviceId === VIRTUAL_DEVICE ||
new URLSearchParams(location.search).get('write') === 'true'
const isVirtualDevice = deviceId === VIRTUAL_DEVICE
// fetch device configuration and data
useEffect(() => {
const fetchData = async () => {
setLoading(true)
try {
const deviceConfig = await fetchDeviceConfig(deviceId)
setDeviceData(await fetchDeviceData(deviceConfig))
} catch (e) {
console.error(e)
setMessage({
title: 'Cannot load device data',
description: String(e),
type: 'error',
})
} finally {
setLoading(false)
}
}
fetchData()
}, [dataStamp, deviceId])
async function writeData() {
const onProgress: ProgressFn = (percent /*, current, total */) => {
// console.log(
// `writeData ${current}/${total} (${Math.trunc(percent * 100) / 100}%)`
// );
setProgress(percent)
}
try {
if (!deviceData) return
const missingDataTimeStamps = mqttEnabled
? await fetchDeviceMissingDataTimeStamps(deviceData.config)
: undefined
const count = await writeEmulatedData(
deviceData,
onProgress,
missingDataTimeStamps
)
if (count) {
notification.success({
message: (
<>
<b>{count}</b> measurement point{count > 1 ? 's were' : ' was'}{' '}
written to InfluxDB.
</>
),
})
setDataStamp(dataStamp + 1) // reload device data
} else {
notification.info({
message: `No new data were written to InfluxDB, the current measurement is already written.`,
})
}
} catch (e) {
console.error(e)
setMessage({
title: 'Cannot write data',
description: String(e),
type: 'error',
})
} finally {
setProgress(-1)
}
}
const writeButtonDisabled = progress !== -1 || loading
const pageControls = (
<>
{writeAllowed ? (
<Tooltip title="Write Missing Data for the last 7 days" placement="top">
<Button
type="primary"
onClick={writeData}
disabled={writeButtonDisabled}
icon={<IconWriteData />}
/>
</Tooltip>
) : undefined}
<Tooltip title="Reload" placement="topRight">
<Button
disabled={loading}
loading={loading}
onClick={() => setDataStamp(dataStamp + 1)}
icon={<IconRefresh />}
/>
</Tooltip>
<Tooltip title="Go to device realtime dashboard" placement="topRight">
<Button
type={mqttEnabled ? 'default' : 'ghost'}
icon={<PlayCircleOutlined />}
href={`/realtime/${deviceId}`}
></Button>
</Tooltip>
<Tooltip title="Go to device dashboard" placement="topRight">
<Button
icon={<IconDashboard />}
href={`/dashboard/${deviceId}`}
></Button>
</Tooltip>
</>
)
const columnDefinitions: ColumnsType<measurementSummaryRow> = [
{
title: 'Field',
dataIndex: '_field',
},
{
title: 'min',
dataIndex: 'minValue',
render: (val: number) => +val.toFixed(2),
align: 'right',
},
{
title: 'max',
dataIndex: 'maxValue',
render: (val: number) => +val.toFixed(2),
align: 'right',
},
{
title: 'max time',
dataIndex: 'maxTime',
},
{
title: 'entry count',
dataIndex: 'count',
align: 'right',
},
{
title: 'sensor',
dataIndex: 'sensor',
},
]
return (
<PageContent
title={
isVirtualDevice ? (
<>
{'Virtual Device'}
<Tooltip title="This page writes temperature measurements for the last 7 days from an emulated device, the temperature is reported every minute.">
<InfoCircleFilled style={{fontSize: '1em', marginLeft: 5}} />
</Tooltip>
</>
) : (
`Device ${deviceId}`
)
}
message={message}
spin={loading}
titleExtra={pageControls}
>
{deviceId === VIRTUAL_DEVICE ? (
<>
<div style={{visibility: progress >= 0 ? 'visible' : 'hidden'}}>
<Progress
percent={progress >= 0 ? Math.trunc(progress) : 0}
strokeColor={COLOR_LINK}
/>
</div>
</>
) : undefined}
<GridDescription
title="Device Configuration"
column={
helpCollapsed ? {xxl: 3, xl: 2, md: 1, sm: 1} : {xxl: 2, md: 1, sm: 1}
}
descriptions={[
{
label: 'Device ID',
value: deviceData?.config.id,
},
{
label: 'Registration Time',
value: deviceData?.config.createdAt,
},
{
label: 'InfluxDB URL',
value: deviceData?.config.influx_url,
},
{
label: 'InfluxDB Organization',
value: deviceData?.config.influx_org,
},
{
label: 'InfluxDB Bucket',
value: deviceData?.config.influx_bucket,
},
{
label: 'InfluxDB Token',
value: deviceData?.config.influx_token ? '***' : 'N/A',
},
...(mqttEnabled
? [
{
label: 'Mqtt URL',
value: deviceData?.config?.mqtt_url,
},
{
label: 'Mqtt topic',
value: deviceData?.config?.mqtt_topic,
},
]
: []),
{
label: 'Device type',
value: (
<InputConfirm
value={deviceData?.config?.device}
tooltip={'Device type is used for dynamic dashboard filtering'}
onValueChange={async (newValue) => {
try {
await fetchSetDeviceType(deviceId, newValue)
setDataStamp(dataStamp + 1)
} catch (e) {
console.error(e)
setMessage({
title: 'Cannot load device data',
description: String(e),
type: 'error',
})
}
}}
/>
),
},
]}
/>
<Title>Measurements</Title>
<Table
dataSource={deviceData?.measurements}
columns={columnDefinitions}
pagination={false}
rowKey={measurementTableRowKey}
/>
<div style={{height: 20}} />
{isVirtualDevice && mqttEnabled ? (
<RealTimeSettings onBeforeStart={writeData} />
) : undefined}
</PageContent>
)
}
Example #24
Source File: index.tsx From antdp with MIT License | 4 votes |
EditableTable = (
props: EditableTableProps,
ref: React.ForwardedRef<RefEditTableProps>,
) => {
const {
columns,
dataSource = [],
onBeforeSave,
onSave,
rowKey = 'id',
optIsFirst = false,
optConfig = {},
isOptDelete = false,
initValue = {},
onValuesChange,
isAdd,
onErr,
multiple = false,
onBeforeAdd,
isOpt = true,
addBtnProps = {},
store,
...rest
} = props;
const [formsRef] = useStore(store)
const [editingKey, setEditingKey] = useState<string[]>([]);
const [newAdd, setNewAdd] = React.useState<string[]>([]);
/** editingKey 和 newAdd 移出 id */
const removeKey = (id: string | number) => {
setEditingKey((arr) => arr.filter((k) => `${k}` !== `${id}`));
setNewAdd((arr) => arr.filter((k) => `${k}` !== `${id}`));
};
/** 获取行 所有编辑字段 */
const fields: string[] = React.useMemo(() => {
return columns
.filter((item) => {
return item.editable;
})
.map((item) => item.dataIndex as string);
}, [columns]);
/** 重置 某个表单 */
const restForm = (key: string | number, obj = {}) => {
const stores = formsRef.getStore();
if (stores[`${key}`]) {
stores[`${key}`].setFieldsValue(obj);
}
};
/** 获取某个表单 */
const getForm = (id: string | number) => {
const stores = formsRef.getStore();
return stores[`${id}`];
};
/** 判断是否编辑 */
const isEditing = (record: any) => editingKey.includes(`${record[rowKey]}`);
/** 判断是否是新增的 */
const isAddEdit = (record: any) => newAdd.includes(`${record[rowKey]}`);
/** 新增 */
const add = () => {
// 新增之前的调用方法
if (onBeforeAdd && !onBeforeAdd()) {
return;
}
if (newAdd.length === 1 && !multiple) {
message.warn('只能新增一行');
return;
}
if (editingKey.length === 1 && !multiple) {
message.warn('只能编辑一行');
return;
}
const id = (new Date().getTime() * Math.round(10)).toString();
const newItem = { ...(initValue || {}), [rowKey]: id };
const list = dataSource.concat([newItem]);
setEditingKey((arr) => arr.concat([id]));
setNewAdd((arr) => arr.concat([id]));
onSave && onSave(list, newItem);
};
/** 编辑 */
const edit = (record: any) => {
let obj = { ...record };
restForm(record[rowKey], obj);
setEditingKey((arr) => arr.concat([`${record[rowKey]}`]));
};
/** 取消编辑 */
const cancel = (id: string | number) => {
removeKey(id);
restForm(id, {});
};
/** 删除行 */
const onDelete = (id: string | number, rowItem: object, index: number) => {
const list = dataSource.filter((item) => `${item[rowKey]}` !== `${id}`);
removeKey(id);
onSave && onSave(list, rowItem, rowItem, index);
};
/** 保存 */
const save = async (key: string | number, record: object, indx: number) => {
try {
const row = await getForm(key).validateFields();
if (onBeforeSave && !onBeforeSave(row, record, indx)) {
return;
}
const newData = [...dataSource];
const index = newData.findIndex((item) => `${key}` === `${item[rowKey]}`);
if (index > -1) {
const item = newData[index];
newData.splice(index, 1, { ...item, ...row });
} else {
newData.push(row);
}
onSave && onSave(newData, row, record, indx);
removeKey(key);
getForm(key).resetFields(fields);
} catch (errInfo) {
onErr && onErr(errInfo as ValidateErrorEntity<any>);
}
};
/** 操作列配置 */
const operation: ColumnsProps[] =
(isOpt &&
Operation({
optConfig,
isEditing,
isAddEdit,
save,
isOptDelete,
cancel,
onDelete,
edit,
newAdd,
editingKey,
rowKey,
multiple,
})) ||
[];
const optColumns = optIsFirst
? operation.concat(columns)
: columns.concat(operation);
const mergedColumns = optColumns.map((col) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record: any) => ({
record,
multiple,
rowKey,
dataIndex: col.dataIndex,
title: col.title,
editing: isEditing(record),
inputNode: col.inputNode,
rules: col.rules || [],
itemAttr: col.itemAttr,
type: col.type,
attr: col.attr,
tip: col.tip,
tipAttr: col.tipAttr,
isList: col.isList,
listAttr: col.listAttr,
}),
};
}) as ColumnsType<any>;
// 表单值更新 表单更新值适用单个 不使用多个
const onChange = (
id: string | number,
form: FormInstance,
value: any,
allValue: any,
) => {
if (onValuesChange) {
const list = dataSource.map((item) => {
if (`${id}` === `${item[rowKey]}`) {
return { ...item, ...allValue };
}
return { ...item };
});
onValuesChange(list, value, allValue, id, form);
}
};
React.useImperativeHandle(
ref,
(): RefEditTableProps => ({
save,
onDelete,
edit,
cancel,
add,
isEditing,
editingKey,
newAdd,
forms: formsRef,
}),
);
return (
<React.Fragment>
<EditForms.Provider
value={{
formsRef,
onValuesChange: onChange,
dataSource,
rowKey,
}}
>
<Table
size="small"
bordered
{...rest}
components={{
body: {
row: Tr,
cell: Td,
},
}}
rowKey={rowKey}
dataSource={dataSource}
columns={mergedColumns}
rowClassName="editable-row"
pagination={false}
/>
{isAdd && (
<Button
type="dashed"
block
children="添加一行数据"
{...(addBtnProps || {})}
style={{ marginTop: 10, ...((addBtnProps || {}).style || {}) }}
onClick={add}
/>
)}
</EditForms.Provider>
</React.Fragment>
);
}
Example #25
Source File: RetentionTable.tsx From posthog-foss with MIT License | 4 votes |
export function RetentionTable({ dashboardItemId = null }: { dashboardItemId?: number | null }): JSX.Element | null {
const { insightProps } = useValues(insightLogic)
const logic = retentionTableLogic(insightProps)
const {
results: _results,
resultsLoading,
peopleLoading,
people: _people,
loadingMore,
filters: { period, date_to, breakdowns },
aggregationTargetLabel,
} = useValues(logic)
const results = _results as RetentionTablePayload[]
const people = _people as RetentionTablePeoplePayload
const { loadPeople, loadMorePeople } = useActions(logic)
const [modalVisible, setModalVisible] = useState(false)
const [selectedRow, selectRow] = useState(0)
const [isLatestPeriod, setIsLatestPeriod] = useState(false)
useEffect(() => {
setIsLatestPeriod(periodIsLatest(date_to || null, period || null))
}, [date_to, period])
const columns: ColumnsType<Record<string, any>> = [
{
title: 'Cohort',
key: 'cohort',
render: (row: RetentionTablePayload) =>
// If we have breakdowns, then use the returned label attribute
// as the cohort name, otherwise we construct one ourselves
// based on the returned date. It might be nice to just unify to
// have label computed as such from the API.
breakdowns?.length
? row.label
: period === 'Hour'
? dayjs(row.date).format('MMM D, h A')
: dayjs.utc(row.date).format('MMM D'),
align: 'center',
},
{
title: 'Cohort Size',
key: 'users',
render: (row) => row.values[0]['count'],
align: 'center',
},
]
if (!resultsLoading && results) {
if (results.length === 0) {
return null
}
const maxIntervalsCount = Math.max(...results.map((result) => result.values.length))
columns.push(
...Array.from(Array(maxIntervalsCount).keys()).map((index: number) => ({
key: `period::${index}`,
title: `${period} ${index}`,
render: (row: RetentionTablePayload) => {
if (index >= row.values.length) {
return ''
}
return renderPercentage(
row.values[index]['count'],
row.values[0]['count'],
isLatestPeriod && index === row.values.length - 1,
index === 0
)
},
}))
)
}
function dismissModal(): void {
setModalVisible(false)
}
function loadMore(): void {
loadMorePeople()
}
return (
<>
<Table
data-attr="retention-table"
size="small"
className="retention-table"
pagination={false}
rowClassName={dashboardItemId ? '' : 'cursor-pointer'}
dataSource={results}
columns={columns}
rowKey="date"
loading={resultsLoading}
onRow={(_, rowIndex: number | undefined) => ({
onClick: () => {
if (!dashboardItemId && rowIndex !== undefined) {
loadPeople(rowIndex)
setModalVisible(true)
selectRow(rowIndex)
}
},
})}
/>
{results && (
<RetentionModal
results={results}
actors={people}
selectedRow={selectedRow}
visible={modalVisible}
dismissModal={dismissModal}
actorsLoading={peopleLoading}
loadMore={loadMore}
loadingMore={loadingMore}
aggregationTargetLabel={aggregationTargetLabel}
/>
)}
</>
)
}
Example #26
Source File: FunnelStepTable.tsx From posthog-foss with MIT License | 4 votes |
export function FunnelStepTable(): JSX.Element | null {
const { insightProps, isViewedOnDashboard } = useValues(insightLogic)
const logic = funnelLogic(insightProps)
const {
stepsWithCount,
flattenedSteps,
steps,
visibleStepsWithConversionMetrics,
hiddenLegendKeys,
barGraphLayout,
flattenedStepsByBreakdown,
flattenedBreakdowns,
aggregationTargetLabel,
isModalActive,
filters,
} = useValues(logic)
const { openPersonsModalForStep, toggleVisibilityByBreakdown, setHiddenById } = useActions(logic)
const { cohorts } = useValues(cohortsModel)
const showLabels = false // #7653 - replaces (visibleStepsWithConversionMetrics?.[0]?.nested_breakdown?.length ?? 0) < 6
const isOnlySeries = flattenedBreakdowns.length === 1
function getColumns(): ColumnsType<FlattenedFunnelStep> | ColumnsType<FlattenedFunnelStepByBreakdown> {
if (barGraphLayout === FunnelLayout.vertical) {
const _columns: ColumnsType<FlattenedFunnelStepByBreakdown> = []
if (!isViewedOnDashboard) {
_columns.push({
render: function RenderCheckbox({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
const checked = !!flattenedBreakdowns?.every(
(b) =>
!hiddenLegendKeys[
getVisibilityIndex(visibleStepsWithConversionMetrics?.[0], b.breakdown_value)
]
)
const color = getSeriesColor(breakdown?.breakdownIndex, isOnlySeries)
return renderGraphAndHeader(
rowIndex,
0,
<PHCheckbox
color={color}
checked={
!hiddenLegendKeys[
getVisibilityIndex(
visibleStepsWithConversionMetrics?.[0],
breakdown.breakdown_value
)
]
} // assume visible status from first step's visibility
onChange={() => toggleVisibilityByBreakdown(breakdown.breakdown_value)}
/>,
<PHCheckbox
color={isOnlySeries ? 'var(--primary)' : undefined}
checked={checked}
indeterminate={flattenedBreakdowns?.some(
(b) =>
!hiddenLegendKeys[
getVisibilityIndex(
visibleStepsWithConversionMetrics?.[0],
b.breakdown_value
)
]
)}
onChange={() => {
// either toggle all data on or off
setHiddenById(
Object.fromEntries(
visibleStepsWithConversionMetrics.flatMap((s) =>
flattenedBreakdowns.map((b) => [
getVisibilityIndex(s, b.breakdown_value),
checked,
])
)
)
)
}}
/>,
showLabels,
undefined,
isViewedOnDashboard
)
},
fixed: 'left',
width: 20,
align: 'center',
})
_columns.push({
render: function RenderLabel({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
const color = getSeriesColor(breakdown?.breakdownIndex, isOnlySeries)
return renderGraphAndHeader(
rowIndex,
1,
<InsightLabel
seriesColor={color}
fallbackName={formatBreakdownLabel(
cohorts,
isOnlySeries ? `Unique ${aggregationTargetLabel.plural}` : breakdown.breakdown_value
)}
hasMultipleSeries={steps.length > 1}
breakdownValue={breakdown.breakdown_value}
hideBreakdown={false}
iconSize={IconSize.Small}
iconStyle={{ marginRight: 12 }}
allowWrap
hideIcon
pillMaxWidth={165}
/>,
renderColumnTitle('Breakdown'),
showLabels,
undefined,
isViewedOnDashboard
)
},
fixed: 'left',
width: 150,
className: 'funnel-table-cell breakdown-label-column',
})
_columns.push({
render: function RenderCompletionRate({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
return renderGraphAndHeader(
rowIndex,
2,
<span>{formatDisplayPercentage(breakdown?.conversionRates?.total ?? 0)}%</span>,
renderSubColumnTitle('Rate'),
showLabels,
undefined,
isViewedOnDashboard
)
},
fixed: 'left',
width: 120,
align: 'right',
className: 'funnel-table-cell dividing-column',
})
}
// Add columns per step
visibleStepsWithConversionMetrics.forEach((step, stepIndex) => {
_columns.push({
render: function RenderCompleted({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
const breakdownStep = breakdown.steps?.[step.order]
return renderGraphAndHeader(
rowIndex,
step.order === 0 ? 3 : (stepIndex - 1) * 5 + 5,
breakdownStep?.count != undefined ? (
<ValueInspectorButton
onClick={() => openPersonsModalForStep({ step: breakdownStep, converted: true })}
disabled={!isModalActive}
>
{breakdown.steps?.[step.order].count}
</ValueInspectorButton>
) : (
EmptyValue
),
renderSubColumnTitle(
<>
<UserOutlined
title={`Unique ${aggregationTargetLabel.plural} ${
filters.aggregation_group_type_index != undefined ? 'that' : 'who'
} completed this step`}
/>{' '}
Completed
</>
),
showLabels,
step,
isViewedOnDashboard
)
},
width: 80,
align: 'right',
})
_columns.push({
render: function RenderConversion({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
return renderGraphAndHeader(
rowIndex,
step.order === 0 ? 4 : (stepIndex - 1) * 5 + 6,
breakdown.steps?.[step.order]?.conversionRates.fromBasisStep != undefined ? (
<>
{getSignificanceFromBreakdownStep(breakdown, step.order)?.fromBasisStep ? (
<Tooltip title="Significantly different from other breakdown values">
<span className="table-text-highlight">
<FlagOutlined style={{ marginRight: 2 }} />{' '}
{formatDisplayPercentage(
breakdown.steps?.[step.order]?.conversionRates.fromBasisStep
)}
%
</span>
</Tooltip>
) : (
<span>
{formatDisplayPercentage(
breakdown.steps?.[step.order]?.conversionRates.fromBasisStep
)}
%
</span>
)}
</>
) : (
EmptyValue
),
renderSubColumnTitle('Rate'),
showLabels,
step,
isViewedOnDashboard
)
},
width: 80,
align: 'right',
className: step.order === 0 ? 'funnel-table-cell dividing-column' : undefined,
})
if (step.order !== 0) {
_columns.push({
render: function RenderDropoff({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
const breakdownStep = breakdown.steps?.[step.order]
return renderGraphAndHeader(
rowIndex,
(stepIndex - 1) * 5 + 7,
breakdownStep?.droppedOffFromPrevious != undefined ? (
<ValueInspectorButton
onClick={() =>
openPersonsModalForStep({
step: breakdownStep,
converted: false,
})
}
disabled={!isModalActive}
>
{breakdown.steps?.[step.order]?.droppedOffFromPrevious}
</ValueInspectorButton>
) : (
EmptyValue
),
renderSubColumnTitle(
<>
<UserDeleteOutlined
title={`Unique ${aggregationTargetLabel.plural} who dropped off on this step`}
/>{' '}
Dropped
</>
),
showLabels,
step,
isViewedOnDashboard
)
},
width: 80,
align: 'right',
})
_columns.push({
render: function RenderDropoffRate({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
return renderGraphAndHeader(
rowIndex,
(stepIndex - 1) * 5 + 8,
breakdown.steps?.[step.order]?.conversionRates.fromPrevious != undefined ? (
<>
{!getSignificanceFromBreakdownStep(breakdown, step.order)?.fromBasisStep &&
getSignificanceFromBreakdownStep(breakdown, step.order)?.fromPrevious ? (
<Tooltip title="Significantly different from other breakdown values">
<span className="table-text-highlight">
<FlagOutlined style={{ marginRight: 2 }} />{' '}
{formatDisplayPercentage(
1 - breakdown.steps?.[step.order]?.conversionRates.fromPrevious
)}
%
</span>
</Tooltip>
) : (
<span>
{formatDisplayPercentage(
1 - breakdown.steps?.[step.order]?.conversionRates.fromPrevious
)}
%
</span>
)}
</>
) : (
EmptyValue
),
renderSubColumnTitle('Rate'),
showLabels,
step,
isViewedOnDashboard
)
},
width: 80,
align: 'right',
})
_columns.push({
render: function RenderAverageTime({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
return renderGraphAndHeader(
rowIndex,
(stepIndex - 1) * 5 + 9,
breakdown.steps?.[step.order]?.average_conversion_time != undefined ? (
<span>
{colonDelimitedDuration(
breakdown.steps?.[step.order]?.average_conversion_time,
3
)}
</span>
) : (
EmptyValue
),
renderSubColumnTitle('Avg. time'),
showLabels,
step,
isViewedOnDashboard
)
},
width: 80,
align: 'right',
className: 'funnel-table-cell dividing-column',
})
}
})
return _columns
}
// If steps are horizontal, render table with flattened steps
const _columns: ColumnsType<FlattenedFunnelStep> = []
_columns.push({
title: '',
render: function RenderSeriesGlyph({}, step: FlattenedFunnelStep): JSX.Element | null {
if (step.breakdownIndex === undefined) {
// Not a breakdown value; show a step-order glyph
return <SeriesGlyph variant="funnel-step-glyph">{humanizeOrder(step.order)}</SeriesGlyph>
}
return null
},
fixed: 'left',
width: 20,
align: 'center',
})
if (!!steps[0]?.breakdown) {
_columns.push({
title: '',
render: function RenderCheckbox({}, step: FlattenedFunnelStep): JSX.Element | null {
const color = getSeriesColor(step?.breakdownIndex, flattenedBreakdowns.length === 1)
// Breakdown parent
if (step.breakdownIndex === undefined && (step.nestedRowKeys ?? []).length > 0) {
return (
<PHCheckbox
checked={!!step.nestedRowKeys?.every((rowKey) => !hiddenLegendKeys[rowKey])}
indeterminate={step.nestedRowKeys?.some((rowKey) => !hiddenLegendKeys[rowKey])}
onChange={() => {
// either toggle all data on or off
const currentState = !!step.nestedRowKeys?.every(
(rowKey) => !hiddenLegendKeys[rowKey]
)
setHiddenById(
Object.fromEntries(
step.nestedRowKeys?.map((rowKey) => [rowKey, currentState]) ?? []
)
)
}}
/>
)
}
// Breakdown child
return (
<PHCheckbox
color={color}
checked={!hiddenLegendKeys[step.rowKey]}
onChange={() => {
setHiddenById({
[getVisibilityIndex(step, step.breakdown_value)]: !hiddenLegendKeys[step.rowKey],
})
}}
/>
)
},
fixed: 'left',
width: 20,
align: 'center',
})
}
_columns.push({
title: 'Step',
render: function RenderLabel({}, step: FlattenedFunnelStep): JSX.Element {
const color = getStepColor(step, !!step.breakdown)
return (
<InsightLabel
seriesColor={color}
fallbackName={
!step.isBreakdownParent && isBreakdownChildType(step.breakdown)
? formatBreakdownLabel(cohorts, step.breakdown)
: step.name
}
action={
!step.isBreakdownParent && isBreakdownChildType(step.breakdown)
? undefined
: getActionFilterFromFunnelStep(step)
}
hasMultipleSeries={step.isBreakdownParent && steps.length > 1}
breakdownValue={
step.breakdown === ''
? 'None'
: isBreakdownChildType(step.breakdown)
? step.breakdown_value
: undefined
}
pillMaxWidth={165}
hideBreakdown={step.isBreakdownParent}
iconSize={IconSize.Small}
iconStyle={{ marginRight: 12 }}
hideIcon
allowWrap
showEventName={step.isBreakdownParent}
/>
)
},
fixed: 'left',
width: 120,
})
_columns.push({
title: 'Completed',
render: function RenderCompleted({}, step: FlattenedFunnelStep): JSX.Element {
return (
<ValueInspectorButton
onClick={() => openPersonsModalForStep({ step, converted: true })}
disabled={!isModalActive}
>
{step.count}
</ValueInspectorButton>
)
},
width: 80,
align: 'center',
})
_columns.push({
title: 'Conversion',
render: function RenderConversion({}, step: FlattenedFunnelStep): JSX.Element | null {
return step.order === 0 ? (
EmptyValue
) : (
<span>{formatDisplayPercentage(step.conversionRates.total)}%</span>
)
},
width: 80,
align: 'center',
})
_columns.push({
title: 'Dropped off',
render: function RenderDropoff({}, step: FlattenedFunnelStep): JSX.Element | null {
return step.order === 0 ? (
EmptyValue
) : (
<ValueInspectorButton
onClick={() => openPersonsModalForStep({ step, converted: false })}
disabled={!isModalActive}
>
{step.droppedOffFromPrevious}
</ValueInspectorButton>
)
},
width: 80,
align: 'center',
})
_columns.push({
title: 'From previous step',
render: function RenderDropoffFromPrevious({}, step: FlattenedFunnelStep): JSX.Element | null {
return step.order === 0 ? (
EmptyValue
) : (
<span>{formatDisplayPercentage(1 - step.conversionRates.fromPrevious)}%</span>
)
},
width: 80,
align: 'center',
})
_columns.push({
title: 'Average time',
render: function RenderAverageTime({}, step: FlattenedFunnelStep): JSX.Element {
return step.average_conversion_time ? (
<span>{humanFriendlyDuration(step.average_conversion_time, 2)}</span>
) : (
EmptyValue
)
},
width: 100,
align: 'center',
})
return _columns
}
// If the bars are vertical, use table as legend #5733
const columns = getColumns()
const tableData: TableProps<any /* TODO: Type this */> =
barGraphLayout === FunnelLayout.vertical
? {
dataSource: flattenedStepsByBreakdown.slice(
0,
isViewedOnDashboard ? 2 : flattenedStepsByBreakdown.length
),
columns,
showHeader: false,
rowClassName: (record, index) => {
return clsx(
`funnel-steps-table-row-${index}`,
index === 2 && 'funnel-table-cell',
record.significant && 'table-cell-highlight'
)
},
}
: {
dataSource: flattenedSteps,
columns,
}
return stepsWithCount.length > 1 ? (
<Table
{...tableData}
scroll={isViewedOnDashboard ? undefined : { x: 'max-content' }}
size="small"
rowKey="rowKey"
pagination={{ pageSize: 100, hideOnSinglePage: true }}
style={{ height: '100%' }}
data-attr={barGraphLayout === FunnelLayout.vertical ? 'funnel-bar-graph' : 'funnel-steps-table'}
className="funnel-steps-table"
/>
) : null
}
Example #27
Source File: VolumeTable.tsx From posthog-foss with MIT License | 4 votes |
export function VolumeTable({
type,
data,
}: {
type: EventTableType
data: Array<EventDefinition | PropertyDefinition>
}): JSX.Element {
const [searchTerm, setSearchTerm] = useState(false as string | false)
const [dataWithWarnings, setDataWithWarnings] = useState([] as VolumeTableRecord[])
const { user } = useValues(userLogic)
const { openDrawer } = useActions(definitionDrawerLogic)
const hasTaxonomyFeatures = user?.organization?.available_features?.includes(AvailableFeature.INGESTION_TAXONOMY)
const columns: ColumnsType<VolumeTableRecord> = [
{
title: `${capitalizeFirstLetter(type)} name`,
render: function Render(_, record): JSX.Element {
return (
<span>
<div style={{ display: 'flex', alignItems: 'baseline', paddingBottom: 4 }}>
<span className="ph-no-capture" style={{ paddingRight: 8 }}>
<PropertyKeyInfo
style={hasTaxonomyFeatures ? { fontWeight: 'bold' } : {}}
value={record.eventOrProp.name}
/>
</span>
{hasTaxonomyFeatures ? (
<ObjectTags tags={record.eventOrProp.tags || []} staticOnly />
) : null}
</div>
{hasTaxonomyFeatures &&
(isPosthogEvent(record.eventOrProp.name) ? null : (
<VolumeTableRecordDescription
id={record.eventOrProp.id}
description={record.eventOrProp.description}
type={type}
/>
))}
{record.warnings?.map((warning) => (
<Tooltip
key={warning}
color="orange"
title={
<>
<b>Warning!</b> {warning}
</>
}
>
<WarningOutlined style={{ color: 'var(--warning)', marginLeft: 6 }} />
</Tooltip>
))}
</span>
)
},
sorter: (a, b) => ('' + a.eventOrProp.name).localeCompare(b.eventOrProp.name || ''),
filters: [
{ text: 'Has warnings', value: 'warnings' },
{ text: 'No warnings', value: 'noWarnings' },
],
onFilter: (value, record) => (value === 'warnings' ? !!record.warnings.length : !record.warnings.length),
},
type === 'event' && hasTaxonomyFeatures
? {
title: 'Owner',
render: function Render(_, record): JSX.Element {
const owner = record.eventOrProp?.owner
return isPosthogEvent(record.eventOrProp.name) ? <>-</> : <Owner user={owner} />
},
}
: {},
type === 'event'
? {
title: function VolumeTitle() {
return (
<Tooltip
placement="right"
title="Total number of events over the last 30 days. Can be delayed by up to an hour."
>
30 day volume (delayed by up to an hour)
<InfoCircleOutlined className="info-indicator" />
</Tooltip>
)
},
render: function RenderVolume(_, record) {
return <span className="ph-no-capture">{compactNumber(record.eventOrProp.volume_30_day)}</span>
},
sorter: (a, b) =>
a.eventOrProp.volume_30_day == b.eventOrProp.volume_30_day
? (a.eventOrProp.volume_30_day || -1) - (b.eventOrProp.volume_30_day || -1)
: (a.eventOrProp.volume_30_day || -1) - (b.eventOrProp.volume_30_day || -1),
}
: {},
{
title: function QueriesTitle() {
return (
<Tooltip
placement="right"
title={`Number of queries in PostHog that included a filter on this ${type}`}
>
30 day queries (delayed by up to an hour)
<InfoCircleOutlined className="info-indicator" />
</Tooltip>
)
},
render: function Render(_, item) {
return <span className="ph-no-capture">{compactNumber(item.eventOrProp.query_usage_30_day)}</span>
},
sorter: (a, b) =>
a.eventOrProp.query_usage_30_day == b.eventOrProp.query_usage_30_day
? (a.eventOrProp.query_usage_30_day || -1) - (b.eventOrProp.query_usage_30_day || -1)
: (a.eventOrProp.query_usage_30_day || -1) - (b.eventOrProp.query_usage_30_day || -1),
},
hasTaxonomyFeatures
? {
render: function Render(_, item) {
return (
<>
{isPosthogEvent(item.eventOrProp.name) ? null : (
<Button
type="link"
icon={<ArrowRightOutlined style={{ color: '#5375FF' }} />}
onClick={() => openDrawer(type, item.eventOrProp.id)}
/>
)}
</>
)
},
}
: {},
]
useEffect(() => {
setDataWithWarnings(
data.map((eventOrProp: EventOrPropType): VolumeTableRecord => {
const record = { eventOrProp } as VolumeTableRecord
record.warnings = []
if (eventOrProp.name?.endsWith(' ')) {
record.warnings.push(`This ${type} ends with a space.`)
}
if (eventOrProp.name?.startsWith(' ')) {
record.warnings.push(`This ${type} starts with a space.`)
}
return record
}) || []
)
}, [data])
return (
<>
<Input.Search
allowClear
enterButton
style={{ marginTop: '1.5rem', maxWidth: 400, width: 'initial', flexGrow: 1 }}
onChange={(e) => {
setSearchTerm(e.target.value)
}}
placeholder={`Filter ${type === 'property' ? 'properties' : 'events'}....`}
/>
<br />
<br />
<Table
dataSource={searchTerm ? search(dataWithWarnings, searchTerm) : dataWithWarnings}
columns={columns}
rowKey={(item) => item.eventOrProp.name}
size="small"
style={{ marginBottom: '4rem' }}
pagination={{ pageSize: 100, hideOnSinglePage: true }}
onRow={(record) =>
hasTaxonomyFeatures && !isPosthogEvent(record.eventOrProp.name)
? { onClick: () => openDrawer(type, record.eventOrProp.id), style: { cursor: 'pointer' } }
: {}
}
/>
</>
)
}
Example #28
Source File: index.tsx From shippo with MIT License | 4 votes |
Users = () => {
const [data, setData] = useState<IUserExtRoleName[]>()
const [total, setTotal] = useState(0)
const [current, setCurrent] = useState(1)
const [isModalVisible, setIsModalVisible] = useState(false)
const editUserDrawerRef = useRef<EditUserDrawerRef>(null)
const [qq, setQQ] = useState('')
const handleUserCreate = useCallback(async (qq: string) => {
if (!checkQQ(qq)) {
return message.info('QQ号格式错误')
}
try {
const hr = await services.admin.user__create({ email: qq + '@qq.com' })
if (hr.data.success) {
message.success('成功')
}
} catch (error) {
console.log(error)
message.error('失败')
}
}, [])
const columns: ColumnsType<IUserExtRoleName> = [
{
title: 'UID',
dataIndex: 'id',
key: 'id',
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
},
{
title: '昵称',
dataIndex: 'nickname',
key: 'nickname',
},
{
title: '头像',
dataIndex: 'avatar',
key: 'avatar',
render: (value) => {
return <Avatar shape="square" size="small" icon={<UserOutlined />} />
},
},
{
title: '经验',
dataIndex: 'exp',
key: 'exp',
},
{
title: '硬币',
dataIndex: 'coin',
key: 'coin',
},
{
title: '角色名称',
dataIndex: 'roleName',
key: 'roleName',
},
{
title: '注册时间',
dataIndex: 'createdAt',
key: 'createdAt',
},
{
title: '操作',
key: 'action',
render: (_, record) => {
return (
<Space size="middle">
<Button type="link" onClick={() => editUserDrawerRef.current?.open(record)}>
编辑用户
</Button>
</Space>
)
},
},
]
const updateTable = useCallback(() => {
services.user
.find_all({
pageSize: 20,
current,
})
.then((hr) => {
setData(
hr.data.resource.items.map((item) => {
return { ...item, createdAt: formatTimeStr(item.createdAt) }
})
)
setTotal(hr.data.resource.total)
})
}, [current])
useEffect(() => {
updateTable()
}, [updateTable])
return (
<div>
<EditUserDrawer ref={editUserDrawerRef} onClose={() => updateTable()} />
<Space size="middle">
<Button
type="primary"
shape="round"
icon={<PlusOutlined />}
onClick={() => setIsModalVisible(true)}
>
新增邮箱用户
</Button>
</Space>
<Table
rowKey="id"
columns={columns}
dataSource={data}
pagination={{
position: ['bottomCenter'],
pageSize: 20,
total,
current,
showSizeChanger: false,
size: 'default',
onChange: (page: number, pageSize: number) => {
setCurrent(page)
},
}}
size="small"
/>
<Modal
title="新增邮箱用户"
visible={isModalVisible}
onOk={() => {
setIsModalVisible(false)
handleUserCreate(qq)
}}
onCancel={() => setIsModalVisible(false)}
>
<Alert message="只需要输入QQ号即可,不需要后戳。(@qq.com)" type="warning" />
<Input placeholder="QQ号" value={qq} onChange={(event) => setQQ(event.target.value)} />
</Modal>
</div>
)
}
Example #29
Source File: role.tsx From shippo with MIT License | 4 votes |
Page_permission_role: React.FC = () => {
const [data, setData] = useState<IRole[]>()
const editRoleDrawerRef = useRef<EditRoleDrawerRef>(null)
const editRolePolicyDrawerRef = useRef<EditRolePolicyDrawerRef>(null)
const handleDle = useCallback((id: number) => {
confirm({
title: '确认删除?',
icon: <ExclamationCircleOutlined />,
content: '此操作不可逆',
onOk() {
console.log('OK')
services.role.del({ id }).then((hr) => {
if (hr.data.success) {
message.success('成功')
} else {
message.success('失败')
}
})
},
onCancel() {
console.log('Cancel')
},
})
}, [])
const [columns, setColumns] = useState<ColumnsType<IRole>>([
{
title: '角色ID',
dataIndex: 'id',
key: 'id',
},
{
title: '角色名称',
dataIndex: 'roleName',
key: 'roleName',
},
{
title: '描述',
dataIndex: 'remark',
key: 'remark',
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
},
{
title: '操作',
key: 'action',
render: (_, record) => {
return (
<Space size="middle">
<Button
type="link"
onClick={() => {
editRoleDrawerRef.current?.open(record)
}}
>
修改
</Button>
<Button
type="link"
onClick={() => {
handleDle(record.id)
}}
>
删除
</Button>
<Button
type="link"
onClick={() => {
editRolePolicyDrawerRef.current?.open(record)
}}
>
权限策略配置
</Button>
</Space>
)
},
},
])
const updateTable = useCallback(async () => {
const hr = await services.role.find_all()
setData(
hr.data.resource.map((item) => {
return { ...item, createdAt: formatTimeStr(item.createdAt) }
})
)
}, [])
useMount(() => {
updateTable()
})
return (
<div>
<EditRoleDrawer ref={editRoleDrawerRef} onClose={() => updateTable()} />
<EditRolePolicyDrawer ref={editRolePolicyDrawerRef} />
<Space size="middle">
<Button type="primary" onClick={() => editRoleDrawerRef.current?.open()}>
新增角色
</Button>
</Space>
<Table
rowKey="id"
columns={columns}
dataSource={data}
pagination={{ position: ['bottomCenter'] }}
size="small"
/>
</div>
)
}