diff --git a/react/src/components/UserCredentialList.tsx b/react/src/components/UserCredentialList.tsx new file mode 100644 index 0000000000..dab5a9571d --- /dev/null +++ b/react/src/components/UserCredentialList.tsx @@ -0,0 +1,537 @@ +import { + filterEmptyItem, + filterNonNullItems, + transformSorterToOrderString, +} from '../helper'; +import { useUpdatableState } from '../hooks'; +import { useBAIPaginationOptionState } from '../hooks/reactPaginationQueryOptions'; +import BAIPropertyFilter from './BAIPropertyFilter'; +import BAITable from './BAITable'; +import Flex from './Flex'; +import { UserCredentialListDeleteMutation } from './__generated__/UserCredentialListDeleteMutation.graphql'; +import { UserCredentialListModifyMutation } from './__generated__/UserCredentialListModifyMutation.graphql'; +import { UserCredentialListQuery } from './__generated__/UserCredentialListQuery.graphql'; +import { + DeleteOutlined, + InfoCircleOutlined, + LoadingOutlined, + ReloadOutlined, + SettingOutlined, +} from '@ant-design/icons'; +import { + App, + Button, + Popconfirm, + Radio, + Tag, + Tooltip, + Typography, + theme, +} from 'antd'; +import graphql from 'babel-plugin-relay/macro'; +import dayjs from 'dayjs'; +import _ from 'lodash'; +import { BanIcon, PlusIcon, UndoIcon } from 'lucide-react'; +import { useState, useTransition } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLazyLoadQuery, useMutation } from 'react-relay'; + +const UserCredentialList: React.FC = () => { + const { t } = useTranslation(); + const { token } = theme.useToken(); + const { message, modal } = App.useApp(); + + const [fetchKey, updateFetchKey] = useUpdatableState('first'); + const [activeType, setActiveType] = useState<'active' | 'inactive'>('active'); + const [order, setOrder] = useState(undefined); + const [filterString, setFilterString] = useState(); + const [isPendingRefresh, startRefreshTransition] = useTransition(); + const [isActiveTypePending, startActiveTypeTransition] = useTransition(); + const [isPendingPageChange, startPageChangeTransition] = useTransition(); + const [isPendingFilter, startFilterTransition] = useTransition(); + + const { + baiPaginationOption, + tablePaginationOption, + setTablePaginationOption, + } = useBAIPaginationOptionState({ + current: 1, + pageSize: 20, + }); + + const { keypair_list } = useLazyLoadQuery( + graphql` + query UserCredentialListQuery( + $limit: Int! + $offset: Int! + $filter: String + $order: String + $domain_name: String + $email: String + $is_active: Boolean + ) { + keypair_list( + limit: $limit + offset: $offset + filter: $filter + order: $order + domain_name: $domain_name + email: $email + is_active: $is_active + ) { + items { + id + user_id + access_key + is_admin + resource_policy + created_at + rate_limit + num_queries + concurrency_used @since(version: "24.09.0") + } + total_count + } + } + `, + { + limit: baiPaginationOption.limit, + offset: baiPaginationOption.offset, + is_active: activeType === 'active', + filter: filterString, + order, + }, + { fetchKey, fetchPolicy: 'network-only' }, + ); + + const [commitModifyKeypair, isInFlightCommitModifyKeypair] = + useMutation(graphql` + mutation UserCredentialListModifyMutation( + $access_key: String! + $props: ModifyKeyPairInput! + ) { + modify_keypair(access_key: $access_key, props: $props) { + ok + msg + } + } + `); + + const [commitDeleteKeypair, isInFlightCommitDeleteKeypair] = + useMutation(graphql` + mutation UserCredentialListDeleteMutation($access_key: String!) { + delete_keypair(access_key: $access_key) { + ok + msg + } + } + `); + + return ( + + + + { + startActiveTypeTransition(() => { + setActiveType(value.target.value); + setTablePaginationOption({ + current: 1, + pageSize: tablePaginationOption.pageSize, + }); + }); + }} + optionType="button" + options={[ + { + label: 'Active', + value: 'active', + }, + { + label: 'Inactive', + value: 'inactive', + }, + ]} + /> + { + startFilterTransition(() => { + setFilterString(value); + }); + }} + /> + + + + + + + , + }} + dataSource={filterNonNullItems(keypair_list?.items)} + columns={filterEmptyItem([ + { + key: 'userID', + title: t('credential.UserID'), + dataIndex: 'user_id', + fixed: 'left', + // FIXME: sorter is not working + // sorter: true, + }, + { + key: 'accessKey', + title: t('credential.AccessKey'), + dataIndex: 'access_key', + sorter: true, + }, + { + key: 'permission', + title: t('credential.Permission'), + dataIndex: 'is_admin', + render: (isAdmin) => + isAdmin ? ( + <> + admin + user + + ) : ( + user + ), + sorter: true, + }, + { + key: 'keyAge', + title: t('credential.KeyAge'), + dataIndex: 'created_at', + render: (createdAt) => { + return `${dayjs().diff(createdAt, 'day')}${t('credential.Days')}`; + }, + sorter: true, + }, + { + key: 'createdAt', + title: t('credential.CreatedAt'), + dataIndex: 'created_at', + render: (createdAt) => dayjs(createdAt).format('lll'), + sorter: true, + }, + { + key: 'resourcePolicy', + title: t('credential.ResourcePolicy'), + dataIndex: 'resource_policy', + sorter: true, + }, + { + key: 'allocation', + title: t('credential.Allocation'), + render: (record) => { + return ( + + + {record.concurrency_used} + + {t('credential.Sessions')} + + + + {record.rate_limit} + + {t('credential.ReqPer15Min')} + + + + {record.num_queries} + + {t('credential.Queries')} + + + + ); + }, + }, + { + key: 'control', + title: t('general.Control'), + fixed: 'right', + render: (record) => { + return ( + +