|
@@ -62,7 +62,7 @@ const ClientBulkAddModal = lazy(() => import('./ClientBulkAddModal'));
|
|
|
const ClientBulkAdjustModal = lazy(() => import('./ClientBulkAdjustModal'));
|
|
const ClientBulkAdjustModal = lazy(() => import('./ClientBulkAdjustModal'));
|
|
|
const FilterDrawer = lazy(() => import('./FilterDrawer'));
|
|
const FilterDrawer = lazy(() => import('./FilterDrawer'));
|
|
|
const SubLinksModal = lazy(() => import('./SubLinksModal'));
|
|
const SubLinksModal = lazy(() => import('./SubLinksModal'));
|
|
|
-const BulkAssignGroupModal = lazy(() => import('./BulkAssignGroupModal'));
|
|
|
|
|
|
|
+const BulkAddToGroupModal = lazy(() => import('./BulkAddToGroupModal'));
|
|
|
const BulkAttachInboundsModal = lazy(() => import('./BulkAttachInboundsModal'));
|
|
const BulkAttachInboundsModal = lazy(() => import('./BulkAttachInboundsModal'));
|
|
|
const BulkDetachInboundsModal = lazy(() => import('./BulkDetachInboundsModal'));
|
|
const BulkDetachInboundsModal = lazy(() => import('./BulkDetachInboundsModal'));
|
|
|
import { emptyFilters, activeFilterCount } from './filters';
|
|
import { emptyFilters, activeFilterCount } from './filters';
|
|
@@ -71,6 +71,45 @@ import './ClientsPage.css';
|
|
|
|
|
|
|
|
const FILTER_STATE_KEY = 'clientsFilterState';
|
|
const FILTER_STATE_KEY = 'clientsFilterState';
|
|
|
|
|
|
|
|
|
|
+function UngroupIcon() {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <span
|
|
|
|
|
+ style={{
|
|
|
|
|
+ position: 'relative',
|
|
|
|
|
+ display: 'inline-flex',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ width: '1em',
|
|
|
|
|
+ height: '1em',
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <TagsOutlined />
|
|
|
|
|
+ <span
|
|
|
|
|
+ aria-hidden="true"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ position: 'absolute',
|
|
|
|
|
+ inset: 0,
|
|
|
|
|
+ display: 'flex',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ pointerEvents: 'none',
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <span
|
|
|
|
|
+ style={{
|
|
|
|
|
+ display: 'block',
|
|
|
|
|
+ width: '125%',
|
|
|
|
|
+ height: '1.5px',
|
|
|
|
|
+ background: 'currentColor',
|
|
|
|
|
+ transform: 'rotate(-45deg)',
|
|
|
|
|
+ borderRadius: '1px',
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
type Bucket = 'active' | 'deactive' | 'depleted' | 'expiring';
|
|
type Bucket = 'active' | 'deactive' | 'depleted' | 'expiring';
|
|
|
|
|
|
|
|
interface PersistedFilterState {
|
|
interface PersistedFilterState {
|
|
@@ -152,7 +191,7 @@ export default function ClientsPage() {
|
|
|
setQuery,
|
|
setQuery,
|
|
|
inbounds, onlines, loading, fetched, subSettings,
|
|
inbounds, onlines, loading, fetched, subSettings,
|
|
|
ipLimitEnable, tgBotEnable, expireDiff, trafficDiff, pageSize,
|
|
ipLimitEnable, tgBotEnable, expireDiff, trafficDiff, pageSize,
|
|
|
- create, update, remove, bulkDelete, bulkAdjust, bulkAssignGroup, attach, bulkAttach, detach, bulkDetach,
|
|
|
|
|
|
|
+ create, update, remove, bulkDelete, bulkAdjust, bulkAddToGroup, bulkRemoveFromGroup, attach, bulkAttach, detach, bulkDetach,
|
|
|
resetTraffic, resetAllTraffics, delDepleted, setEnable,
|
|
resetTraffic, resetAllTraffics, delDepleted, setEnable,
|
|
|
applyTrafficEvent, applyClientStatsEvent,
|
|
applyTrafficEvent, applyClientStatsEvent,
|
|
|
hydrate,
|
|
hydrate,
|
|
@@ -461,6 +500,26 @@ export default function ClientsPage() {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function onBulkUngroup() {
|
|
|
|
|
+ const emails = [...selectedRowKeys];
|
|
|
|
|
+ if (emails.length === 0) return;
|
|
|
|
|
+ modal.confirm({
|
|
|
|
|
+ title: t('pages.clients.ungroupConfirmTitle', { count: emails.length }),
|
|
|
|
|
+ content: t('pages.clients.ungroupConfirmContent'),
|
|
|
|
|
+ okText: t('confirm'),
|
|
|
|
|
+ okType: 'danger',
|
|
|
|
|
+ cancelText: t('cancel'),
|
|
|
|
|
+ onOk: async () => {
|
|
|
|
|
+ const msg = await bulkRemoveFromGroup(emails);
|
|
|
|
|
+ if (msg?.success) {
|
|
|
|
|
+ setSelectedRowKeys([]);
|
|
|
|
|
+ const affected = (msg.obj as { affected?: number } | undefined)?.affected ?? emails.length;
|
|
|
|
|
+ messageApi.success(t('pages.clients.ungroupSuccessToast', { count: affected }));
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function onBulkDelete() {
|
|
function onBulkDelete() {
|
|
|
const emails = [...selectedRowKeys];
|
|
const emails = [...selectedRowKeys];
|
|
|
if (emails.length === 0) return;
|
|
if (emails.length === 0) return;
|
|
@@ -586,6 +645,7 @@ export default function ClientsPage() {
|
|
|
title: t('pages.clients.group'),
|
|
title: t('pages.clients.group'),
|
|
|
key: 'group',
|
|
key: 'group',
|
|
|
width: 130,
|
|
width: 130,
|
|
|
|
|
+ hidden: allGroups.length === 0,
|
|
|
render: (_v, record) => {
|
|
render: (_v, record) => {
|
|
|
if (!record.group) return <span style={{ color: 'rgba(0,0,0,0.45)' }}>—</span>;
|
|
if (!record.group) return <span style={{ color: 'rgba(0,0,0,0.45)' }}>—</span>;
|
|
|
const isActive = filters.groups.includes(record.group);
|
|
const isActive = filters.groups.includes(record.group);
|
|
@@ -670,7 +730,7 @@ export default function ClientsPage() {
|
|
|
),
|
|
),
|
|
|
},
|
|
},
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
- ], [t, togglingEmail, clientBucket, isOnline, inboundsById, filters]);
|
|
|
|
|
|
|
+ ], [t, togglingEmail, clientBucket, isOnline, inboundsById, filters, allGroups]);
|
|
|
|
|
|
|
|
const tablePagination = {
|
|
const tablePagination = {
|
|
|
current: currentPage,
|
|
current: currentPage,
|
|
@@ -803,6 +863,12 @@ export default function ClientsPage() {
|
|
|
<Button danger icon={<UsergroupDeleteOutlined />} onClick={() => setBulkDetachOpen(true)}>
|
|
<Button danger icon={<UsergroupDeleteOutlined />} onClick={() => setBulkDetachOpen(true)}>
|
|
|
{!isMobile && t('pages.clients.detach')}
|
|
{!isMobile && t('pages.clients.detach')}
|
|
|
</Button>
|
|
</Button>
|
|
|
|
|
+ <Button icon={<TagsOutlined />} onClick={() => setBulkGroupOpen(true)}>
|
|
|
|
|
+ {!isMobile && t('pages.clients.addToGroup')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button danger icon={<UngroupIcon />} onClick={onBulkUngroup}>
|
|
|
|
|
+ {!isMobile && t('pages.clients.ungroup')}
|
|
|
|
|
+ </Button>
|
|
|
</>
|
|
</>
|
|
|
)}
|
|
)}
|
|
|
<Dropdown
|
|
<Dropdown
|
|
@@ -817,12 +883,6 @@ export default function ClientsPage() {
|
|
|
label: t('pages.clients.adjust'),
|
|
label: t('pages.clients.adjust'),
|
|
|
onClick: () => setBulkAdjustOpen(true),
|
|
onClick: () => setBulkAdjustOpen(true),
|
|
|
},
|
|
},
|
|
|
- {
|
|
|
|
|
- key: 'group',
|
|
|
|
|
- icon: <TagsOutlined />,
|
|
|
|
|
- label: t('pages.clients.group'),
|
|
|
|
|
- onClick: () => setBulkGroupOpen(true),
|
|
|
|
|
- },
|
|
|
|
|
{
|
|
{
|
|
|
key: 'subLinks',
|
|
key: 'subLinks',
|
|
|
icon: <LinkOutlined />,
|
|
icon: <LinkOutlined />,
|
|
@@ -1181,13 +1241,13 @@ export default function ClientsPage() {
|
|
|
/>
|
|
/>
|
|
|
</LazyMount>
|
|
</LazyMount>
|
|
|
<LazyMount when={bulkGroupOpen}>
|
|
<LazyMount when={bulkGroupOpen}>
|
|
|
- <BulkAssignGroupModal
|
|
|
|
|
|
|
+ <BulkAddToGroupModal
|
|
|
open={bulkGroupOpen}
|
|
open={bulkGroupOpen}
|
|
|
count={selectedRowKeys.length}
|
|
count={selectedRowKeys.length}
|
|
|
groups={allGroups}
|
|
groups={allGroups}
|
|
|
onOpenChange={setBulkGroupOpen}
|
|
onOpenChange={setBulkGroupOpen}
|
|
|
onSubmit={async (group) => {
|
|
onSubmit={async (group) => {
|
|
|
- const msg = await bulkAssignGroup([...selectedRowKeys], group);
|
|
|
|
|
|
|
+ const msg = await bulkAddToGroup([...selectedRowKeys], group);
|
|
|
if (msg?.success) {
|
|
if (msg?.success) {
|
|
|
setSelectedRowKeys([]);
|
|
setSelectedRowKeys([]);
|
|
|
return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 };
|
|
return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 };
|