mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-19 08:37:14 +00:00
refactor(clients): coherent group management — rename, split, extract
This bundles a set of group-related improvements that built up across one session and only make sense together. Terminology / API surface: - Rename "assign group" → "add to group" everywhere: i18n keys, callback names (bulkAddToGroup), component + file names (BulkAddToGroupModal, AddClientsToGroupModal), Go controller/struct names (bulkAddToGroup, AddToGroup), OpenAPI summaries. Nothing keeps the word "assign" anymore. - Move group routes under /panel/api/clients/groups/* (was /bulkAssignGroup at the clients root). - Split add and remove into two endpoints: /groups/bulkAdd now rejects empty group; new /groups/bulkRemove clears the label for the given emails. The old "submit empty to clear" UX is gone — Ungroup is its own action. UI affordances on Clients page: - Promote Group + Ungroup to visible bar buttons next to Attach + Detach. Group reuses BulkAddToGroupModal; Ungroup pops a danger confirm and calls bulkRemoveFromGroup. - Custom UngroupIcon (TagsOutlined with a diagonal strike) for the Ungroup button so the pairing reads at a glance. - Hide the Group column when no clients have a group label yet — removes a column of em-dashes on fresh installs. UI on Groups page: - New per-row Add clients… / Remove clients… actions backed by GroupAddClientsModal and GroupRemoveClientsModal: rich client picker (email / comment / current group / enable) with search and preserveSelectedRowKeys, mirroring the inbounds Attach modal UX. Controller split: - Move all /groups/* routes, handlers, and request bodies out of web/controller/client.go into a dedicated web/controller/group.go (GroupController with leaner clientService + xrayService dependencies). URLs are byte-identical because the new controller registers on the same parent gin.RouterGroup; api_docs_test.go gets a group.go → /panel/api/clients basePath entry so its route extraction keeps working. Invalidation dedup: - Removing a client from a group on the Groups page used to refetch /clients/groups and /clients/onlines three times: once from the mutation's onSuccess, once from a redundant invalidate() in the page's onSubmit, once from the WebSocket invalidate broadcast that the backend fires after every mutation. The manual invalidate() is gone, and a small invalidationTracker module lets websocketBridge skip WS-driven invalidates that arrive within 1.5s of a local invalidate — bringing the refetch count down to one. The WS path still works for changes made by another tab or user.
This commit is contained in:
@@ -30,6 +30,8 @@ import {
|
||||
RetweetOutlined,
|
||||
TagsOutlined,
|
||||
TeamOutlined,
|
||||
UsergroupAddOutlined,
|
||||
UsergroupDeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
@@ -47,6 +49,8 @@ import { parseMsg } from '@/utils/zodValidate';
|
||||
|
||||
const SubLinksModal = lazy(() => import('../clients/SubLinksModal'));
|
||||
const ClientBulkAdjustModal = lazy(() => import('../clients/ClientBulkAdjustModal'));
|
||||
const GroupAddClientsModal = lazy(() => import('./GroupAddClientsModal'));
|
||||
const GroupRemoveClientsModal = lazy(() => import('./GroupRemoveClientsModal'));
|
||||
|
||||
const JSON_HEADERS = { headers: { 'Content-Type': 'application/json' } } as const;
|
||||
|
||||
@@ -77,7 +81,7 @@ export default function GroupsPage() {
|
||||
useEffect(() => { setMessageInstance(messageApi); }, [messageApi]);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { clients, subSettings, bulkAdjust, bulkDelete } = useClients();
|
||||
const { clients, subSettings, bulkAdjust, bulkAddToGroup, bulkRemoveFromGroup, bulkDelete } = useClients();
|
||||
|
||||
const groupsQuery = useQuery({
|
||||
queryKey: keys.clients.groups(),
|
||||
@@ -124,6 +128,8 @@ export default function GroupsPage() {
|
||||
|
||||
const [subLinksOpen, setSubLinksOpen] = useState(false);
|
||||
const [adjustOpen, setAdjustOpen] = useState(false);
|
||||
const [addClientsOpen, setAddClientsOpen] = useState(false);
|
||||
const [removeClientsOpen, setRemoveClientsOpen] = useState(false);
|
||||
const [groupEmails, setGroupEmails] = useState<string[]>([]);
|
||||
const [groupForAction, setGroupForAction] = useState<GroupSummary | null>(null);
|
||||
|
||||
@@ -228,6 +234,20 @@ export default function GroupsPage() {
|
||||
setAdjustOpen(true);
|
||||
}
|
||||
|
||||
function openAddClientsFor(g: GroupSummary) {
|
||||
setGroupForAction(g);
|
||||
setAddClientsOpen(true);
|
||||
}
|
||||
|
||||
function openRemoveClientsFor(g: GroupSummary) {
|
||||
if (!g.clientCount) {
|
||||
messageApi.info(t('pages.groups.emptyForAction'));
|
||||
return;
|
||||
}
|
||||
setGroupForAction(g);
|
||||
setRemoveClientsOpen(true);
|
||||
}
|
||||
|
||||
function onDeleteClients(g: GroupSummary) {
|
||||
if (!g.clientCount) {
|
||||
messageApi.info(t('pages.groups.emptyForAction'));
|
||||
@@ -306,6 +326,20 @@ export default function GroupsPage() {
|
||||
disabled: !row.clientCount,
|
||||
onClick: () => onResetTraffic(row),
|
||||
},
|
||||
{
|
||||
key: 'addClients',
|
||||
icon: <UsergroupAddOutlined />,
|
||||
label: t('pages.groups.addToGroup'),
|
||||
onClick: () => openAddClientsFor(row),
|
||||
},
|
||||
{
|
||||
key: 'removeClients',
|
||||
icon: <UsergroupDeleteOutlined />,
|
||||
label: t('pages.groups.removeFromGroup'),
|
||||
danger: true,
|
||||
disabled: !row.clientCount,
|
||||
onClick: () => openRemoveClientsFor(row),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: 'rename',
|
||||
@@ -522,6 +556,38 @@ export default function GroupsPage() {
|
||||
}}
|
||||
/>
|
||||
</LazyMount>
|
||||
|
||||
<LazyMount when={addClientsOpen}>
|
||||
<GroupAddClientsModal
|
||||
open={addClientsOpen}
|
||||
groupName={groupForAction?.name ?? null}
|
||||
candidates={clients.filter((c) => c.group !== groupForAction?.name)}
|
||||
onClose={() => setAddClientsOpen(false)}
|
||||
onSubmit={async (emails) => {
|
||||
const msg = await bulkAddToGroup(emails, groupForAction?.name ?? '');
|
||||
if (msg?.success) {
|
||||
return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 };
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</LazyMount>
|
||||
|
||||
<LazyMount when={removeClientsOpen}>
|
||||
<GroupRemoveClientsModal
|
||||
open={removeClientsOpen}
|
||||
groupName={groupForAction?.name ?? null}
|
||||
members={clients.filter((c) => c.group === groupForAction?.name)}
|
||||
onClose={() => setRemoveClientsOpen(false)}
|
||||
onSubmit={async (emails) => {
|
||||
const msg = await bulkRemoveFromGroup(emails);
|
||||
if (msg?.success) {
|
||||
return (msg.obj as { affected?: number } | undefined) ?? { affected: 0 };
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</LazyMount>
|
||||
</Layout>
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user