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 18, 2024
1 parent 2479854 commit ce92025
Show file tree
Hide file tree
Showing 23 changed files with 553 additions and 414 deletions.
587 changes: 314 additions & 273 deletions react/src/components/UserCredentialList.tsx

Large diffs are not rendered by default.

318 changes: 187 additions & 131 deletions react/src/components/UserNodeList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,34 @@ 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,
MoreOutlined,
} from '@ant-design/icons';
import { Tooltip, Button, Table, theme, Radio, Popconfirm, App } from 'antd';
import {
Tooltip,
Button,
Table,
theme,
Radio,
Popconfirm,
App,
TableColumnsType,
Dropdown,
} from 'antd';
import graphql from 'babel-plugin-relay/macro';
import dayjs from 'dayjs';
import _ from 'lodash';
Expand All @@ -26,6 +41,12 @@ import React, { 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 +145,152 @@ 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>
);
},
},
]);

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

return (
<Flex direction="column" align="stretch">
<Flex
Expand Down Expand Up @@ -211,6 +378,22 @@ const UserNodeList: React.FC<UserNodeListProps> = () => {
/>
</Flex>
<Flex gap="xs">
<Dropdown
menu={{
items: [
{
key: 'export-csv',
label: t('credential.ExportCSV'),
onClick: () => {
handleExportCSV();
},
},
],
}}
placement="bottomRight"
>
<Button icon={<MoreOutlined />} />
</Dropdown>
<Tooltip title={t('button.Refresh')}>
<Button
loading={isPendingRefresh}
Expand All @@ -237,134 +420,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 All @@ -375,7 +431,7 @@ const UserNodeList: React.FC<UserNodeListProps> = () => {
showTotal(total, range) {
return `${range[0]}-${range[1]} of ${total} users`;
},
pageSizeOptions: ['10', '20', '50'],
pageSizeOptions: ['10', '20', '50', '100', '500', '1000'],
style: { marginRight: token.marginXS },
}}
onChange={({ pageSize, current }, filters, sorter) => {
Expand Down
4 changes: 3 additions & 1 deletion resources/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,9 @@
"ReqPer15Min": "Erforderlich pro 15 Min",
"Sessions": "Sitzungen",
"KeypairStatusUpdatedSuccessfully": "Der Schlüsselpaarstatus hat sich geändert.",
"KeypairSuccessfullyDeleted": "KeyPair wurde erfolgreich gelöscht."
"KeypairSuccessfullyDeleted": "KeyPair wurde erfolgreich gelöscht.",
"NoDataToExport": "Die Daten für den CSV-Extrakt sind nicht vorhanden.",
"ExportCSV": "CSV exportieren"
},
"data": {
"Folders": "Ordner",
Expand Down
4 changes: 3 additions & 1 deletion resources/i18n/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,9 @@
"ReqPer15Min": "Απαίτηση ανά 15 λεπτά",
"Sessions": "Συνεδρίες",
"KeypairStatusUpdatedSuccessfully": "Η κατάσταση του ζεύγους κλειδιών έχει αλλάξει.",
"KeypairSuccessfullyDeleted": "Το KeyPair διαγράφηκε με επιτυχία."
"KeypairSuccessfullyDeleted": "Το KeyPair διαγράφηκε με επιτυχία.",
"NoDataToExport": "Τα δεδομένα για το απόσπασμα CSV δεν υπάρχουν.",
"ExportCSV": "εξαγωγή CSV"
},
"data": {
"Folders": "Φάκελοι",
Expand Down
Loading

0 comments on commit ce92025

Please sign in to comment.