Skip to content

Commit

Permalink
feat: export users and credential to csv
Browse files Browse the repository at this point in the history
  • Loading branch information
ironAiken2 committed Dec 17, 2024
1 parent b70cc28 commit 0d7d98d
Show file tree
Hide file tree
Showing 24 changed files with 566 additions and 429 deletions.
581 changes: 304 additions & 277 deletions react/src/components/UserCredentialList.tsx

Large diffs are not rendered by default.

311 changes: 180 additions & 131 deletions react/src/components/UserNodeList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,46 @@ import {
filterNonNullItems,
transformSorterToOrderString,
} from '../helper';
import { exportCSVWithFormattingRules } from '../helper/csv-util';
import { useUpdatableState } from '../hooks';
import { useBAIPaginationOptionState } from '../hooks/reactPaginationQueryOptions';
import UserInfoModal from './UserInfoModal';
import UserSettingModal from './UserSettingModal';
import { UserNodeListModifyMutation } from './__generated__/UserNodeListModifyMutation.graphql';
import { UserNodeListQuery } from './__generated__/UserNodeListQuery.graphql';
import {
UserNodeListQuery,
UserNodeListQuery$data,
} from './__generated__/UserNodeListQuery.graphql';
import {
ReloadOutlined,
LoadingOutlined,
InfoCircleOutlined,
SettingOutlined,
} from '@ant-design/icons';
import { Tooltip, Button, Table, theme, Radio, Popconfirm, App } from 'antd';
import {
Tooltip,
Button,
Table,
theme,
Radio,
Popconfirm,
App,
TableColumnsType,
} from 'antd';
import graphql from 'babel-plugin-relay/macro';
import dayjs from 'dayjs';
import _ from 'lodash';
import { BanIcon, PlusIcon, UndoIcon } from 'lucide-react';
import React, { useState, useTransition } from 'react';
import React, { useEffect, useState, useTransition } from 'react';
import { useTranslation } from 'react-i18next';
import { useLazyLoadQuery, useMutation } from 'react-relay';

type UserNode = NonNullable<
NonNullable<
NonNullable<UserNodeListQuery$data['user_nodes']>
>['edges'][number]
>['node'];

interface UserNodeListProps {}

const UserNodeList: React.FC<UserNodeListProps> = () => {
Expand Down Expand Up @@ -124,6 +143,163 @@ const UserNodeList: React.FC<UserNodeListProps> = () => {
}
`);

const columns: TableColumnsType<UserNode> = filterEmptyItem([
{
key: 'email',
title: t('credential.UserID'),
dataIndex: 'email',
sorter: true,
},
{
key: 'username',
title: t('credential.Name'),
dataIndex: 'username',
sorter: true,
},
{
key: 'role',
title: t('credential.Role'),
dataIndex: 'role',
sorter: true,
},
{
key: 'description',
title: t('credential.Description'),
dataIndex: 'description',
},
{
key: 'created_at',
title: t('credential.CreatedAt'),
dataIndex: 'created_at',
render: (text) => dayjs(text).format('lll'),
sorter: true,
defaultSortOrder: 'descend',
},
activeFilter === 'status != "active"' && {
key: 'status',
title: t('credential.Status'),
dataIndex: 'status',
sorter: true,
},
{
key: 'control',
title: t('general.Control'),
render: (value, record) => {
const isActive = record?.status === 'active';
return (
<Flex gap={token.marginXS}>
<Button
type="text"
icon={
<InfoCircleOutlined style={{ color: token.colorSuccess }} />
}
onClick={() => {
startInfoModalOpenTransition(() => {
setEmailForInfoModal(record?.email || null);
});
}}
/>
<Button
type="text"
icon={<SettingOutlined style={{ color: token.colorInfo }} />}
onClick={() => {
startSettingModalOpenTransition(() => {
setEmailForSettingModal(record?.email || null);
});
}}
/>
<Tooltip
title={
isActive ? t('credential.Inactive') : t('credential.Active')
}
>
<Popconfirm
title={
isActive
? t('credential.ConfirmUpdateStatusToInActive')
: t('credential.ConfirmUpdateStatusToActive')
}
placement="left"
okType={isActive ? 'danger' : 'primary'}
okText={isActive ? t('credential.Inactive') : undefined}
description={record?.email}
onConfirm={() => {
setPendingUserId(record?.id || '');
commitModifyUser({
variables: {
email: record?.email || '',
props: {
status: isActive ? 'inactive' : 'active',
},
},
onCompleted: () => {
message.success(
t('credential.StatusUpdatedSuccessfully'),
);
startRefreshTransition(() => {
updateFetchKey();
});
},
onError: (error) => {
message.error(error?.message);
console.error(error);
},
});
}}
>
<Button
type="text"
danger={isActive}
icon={isActive ? <BanIcon /> : <UndoIcon />}
disabled={
isInFlightCommitModifyUser && pendingUserId !== record?.id
}
loading={
isInFlightCommitModifyUser && pendingUserId === record?.id
}
/>
</Popconfirm>
</Tooltip>
</Flex>
);
},
},
]);

useEffect(() => {
const handleExportCSV = () => {
if (!user_nodes?.edges || _.isEmpty(user_nodes?.edges)) {
message.error(t('credential.NoDataToExport'));
return;
}

const columnKeys = _.without(
_.map(columns, (column) => _.toString(column?.key)),
'control',
);
const responseData: Partial<UserNode>[] = _.map(
user_nodes?.edges,
(e) => {
return _.pick(e?.node, columnKeys);
},
);

exportCSVWithFormattingRules(
responseData,
`${activeFilter === 'status == "active"' ? 'active' : 'inactive'}_users_list`,
{
created_at: (value) => dayjs(value).format('lll'),
},
);
};

document.addEventListener('export-csv', handleExportCSV);
return () => {
document.removeEventListener('export-csv', handleExportCSV);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user_nodes, columns, activeFilter]);

return (
<Flex direction="column" align="stretch">
<Flex
Expand Down Expand Up @@ -237,134 +413,7 @@ const UserNodeList: React.FC<UserNodeListProps> = () => {
scroll={{ x: 'max-content' }}
rowKey={'id'}
dataSource={_.map(filterNonNullItems(user_nodes?.edges), (e) => e.node)}
columns={filterEmptyItem([
{
key: 'email',
title: t('credential.UserID'),
dataIndex: 'email',
sorter: true,
},
{
key: 'username',
title: t('credential.Name'),
dataIndex: 'username',
sorter: true,
},
{
key: 'role',
title: t('credential.Role'),
dataIndex: 'role',
sorter: true,
},
{
key: 'description',
title: t('credential.Description'),
dataIndex: 'description',
},
{
title: t('credential.CreatedAt'),
dataIndex: 'created_at',
render: (text) => dayjs(text).format('lll'),
sorter: true,
defaultSortOrder: 'descend',
},
activeFilter === 'status != "active"' && {
key: 'status',
title: t('credential.Status'),
dataIndex: 'status',
sorter: true,
},
{
title: t('general.Control'),
render: (record) => {
const isActive = record?.status === 'active';
return (
<Flex gap={token.marginXS}>
<Button
type="text"
icon={
<InfoCircleOutlined
style={{ color: token.colorSuccess }}
/>
}
onClick={() => {
startInfoModalOpenTransition(() => {
setEmailForInfoModal(record?.email || null);
});
}}
/>
<Button
type="text"
icon={
<SettingOutlined style={{ color: token.colorInfo }} />
}
onClick={() => {
startSettingModalOpenTransition(() => {
setEmailForSettingModal(record?.email || null);
});
}}
/>
<Tooltip
title={
isActive
? t('credential.Inactive')
: t('credential.Active')
}
>
<Popconfirm
title={
isActive
? t('credential.ConfirmUpdateStatusToInActive')
: t('credential.ConfirmUpdateStatusToActive')
}
placement="left"
okType={isActive ? 'danger' : 'primary'}
okText={isActive ? t('credential.Inactive') : undefined}
description={record?.email}
onConfirm={() => {
setPendingUserId(record?.id || '');
commitModifyUser({
variables: {
email: record?.email || '',
props: {
status: isActive ? 'inactive' : 'active',
},
},
onCompleted: () => {
message.success(
t('credential.StatusUpdatedSuccessfully'),
);
startRefreshTransition(() => {
updateFetchKey();
});
},
onError: (error) => {
message.error(error?.message);
console.error(error);
},
});
}}
>
<Button
type="text"
danger={isActive}
icon={isActive ? <BanIcon /> : <UndoIcon />}
disabled={
isInFlightCommitModifyUser &&
pendingUserId !== record?.id
}
loading={
isInFlightCommitModifyUser &&
pendingUserId === record?.id
}
/>
</Popconfirm>
</Tooltip>
</Flex>
);
},
},
])}
columns={columns}
showSorterTooltip={false}
sortDirections={['descend', 'ascend', 'descend']}
pagination={{
Expand Down
19 changes: 19 additions & 0 deletions react/src/pages/UserCredentialsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Flex from '../components/Flex';
import FlexActivityIndicator from '../components/FlexActivityIndicator';
import UserCredentialList from '../components/UserCredentialList';
import UserNodeList from '../components/UserNodeList';
import { MoreOutlined } from '@ant-design/icons';
import { Button, Dropdown } from 'antd';
import { createStyles } from 'antd-style';
import { CardTabListType } from 'antd/es/card';
import { Suspense } from 'react';
Expand Down Expand Up @@ -40,6 +42,23 @@ const UserCredentialsPage: React.FC = () => {
activeTabKey={curTabKey}
onTabChange={setCurTabKey}
tabList={tabItems}
tabBarExtraContent={
<Dropdown
menu={{
items: [
{
key: 'exportCSV',
label: t('credential.ExportCSV'),
onClick: () => {
document.dispatchEvent(new CustomEvent('export-csv'));
},
},
],
}}
>
<Button type="text" style={{ padding: 0 }} icon={<MoreOutlined />} />
</Dropdown>
}
>
<Suspense
fallback={
Expand Down
4 changes: 3 additions & 1 deletion resources/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,9 @@
"LastUsed": "Zuletzt verwendeten",
"Queries": "Abfragen",
"ReqPer15Min": "Erforderlich pro 15 Min",
"Sessions": "Sitzungen"
"Sessions": "Sitzungen",
"NoDataToExport": "Die Daten für den CSV-Extrakt sind nicht vorhanden.",
"ExportCSV": "CSV exportieren"
},
"data": {
"Folders": "Ordner",
Expand Down
Loading

0 comments on commit 0d7d98d

Please sign in to comment.