refactor(group): remove deprecated user group components

Remove unused user group management components as user groups have been deprecated in favor of node groups only.
This commit is contained in:
EUForest 2026-03-08 23:42:39 +08:00
parent ae31019477
commit 4b4edd48e3
4 changed files with 0 additions and 824 deletions

View File

@ -1,210 +0,0 @@
"use client";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@workspace/ui/components/dialog";
import { Input } from "@workspace/ui/components/input";
import { Label } from "@workspace/ui/components/label";
import { Textarea } from "@workspace/ui/components/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select";
import { Switch } from "@workspace/ui/components/switch";
import { Loader2 } from "lucide-react";
import { forwardRef, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
interface UserGroupFormProps {
initialValues?: Partial<API.UserGroup>;
loading?: boolean;
nodeGroups?: API.NodeGroup[];
onSubmit: (values: Record<string, unknown>) => Promise<boolean>;
title: string;
trigger: React.ReactNode;
}
const UserGroupForm = forwardRef<
HTMLButtonElement,
UserGroupFormProps
>(({ initialValues, loading, nodeGroups = [], onSubmit, title, trigger }, ref) => {
const { t } = useTranslation("group");
const [open, setOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [values, setValues] = useState({
name: "",
description: "",
sort: 0,
node_group_id: null as number | null,
for_calculation: true,
});
useEffect(() => {
if (open) {
if (initialValues) {
setValues({
name: initialValues.name || "",
description: initialValues.description || "",
sort: initialValues.sort ?? 0,
node_group_id: initialValues.node_group_id || null,
for_calculation: initialValues.for_calculation ?? true,
});
} else {
setValues({
name: "",
description: "",
sort: 0,
node_group_id: null,
for_calculation: true,
});
}
}
}, [initialValues, open]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitting(true);
const success = await onSubmit(values);
setSubmitting(false);
if (success) {
setOpen(false);
setValues({
name: "",
description: "",
sort: 0,
node_group_id: null,
for_calculation: true,
});
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild ref={ref}>
{trigger}
</DialogTrigger>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>
{t("userGroupFormDescription", "Configure user group settings")}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">
{t("name", "Name")} *
</Label>
<Input
id="name"
value={values.name}
onChange={(e) =>
setValues({ ...values, name: e.target.value })
}
placeholder={t("namePlaceholder", "Enter name")}
required
/>
</div>
<div className="flex items-center justify-between space-x-2">
<Label htmlFor="for_calculation">{t("forCalculation", "For Calculation")}</Label>
<Switch
id="for_calculation"
checked={values.for_calculation}
onCheckedChange={(checked) =>
setValues({ ...values, for_calculation: checked })
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">
{t("description", "Description")}
</Label>
<Textarea
id="description"
value={values.description}
onChange={(e) =>
setValues({ ...values, description: e.target.value })
}
placeholder={t("descriptionPlaceholder", "Enter description")}
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="node_group_id">{t("nodeGroup", "Node Group")}</Label>
<Select
value={values.node_group_id ? String(values.node_group_id) : "0"}
onValueChange={(val) =>
setValues({
...values,
node_group_id: val === "0" ? null : parseInt(val),
})
}
>
<SelectTrigger id="node_group_id" className="w-full">
<SelectValue placeholder={t("selectNodeGroupPlaceholder", "Select a node group...")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">
{t("unbound", "Unbound")}
</SelectItem>
{nodeGroups.map((nodeGroup) => (
<SelectItem key={nodeGroup.id} value={String(nodeGroup.id)}>
{nodeGroup.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="sort">{t("sort", "Sort Order")}</Label>
<Input
id="sort"
type="number"
value={values.sort}
onChange={(e) =>
setValues({ ...values, sort: parseInt(e.target.value) || 0 })
}
min={0}
/>
</div>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => setOpen(false)}
className="rounded-md border px-4 py-2 text-sm"
disabled={submitting || loading}
>
{t("cancel", "Cancel")}
</button>
<button
type="submit"
disabled={submitting || loading}
className="flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground disabled:opacity-50"
>
{submitting && <Loader2 className="h-4 w-4 animate-spin" />}
{t("save", "Save")}
</button>
</div>
</form>
</DialogContent>
</Dialog>
);
});
UserGroupForm.displayName = "UserGroupForm";
export default UserGroupForm;

View File

@ -1,200 +0,0 @@
"use client";
import { Button } from "@workspace/ui/components/button";
import { Badge } from "@workspace/ui/components/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@workspace/ui/components/card";
import { ConfirmButton } from "@workspace/ui/composed/confirm-button";
import {
ProTable,
type ProTableActions,
} from "@workspace/ui/composed/pro-table/pro-table";
import {
createUserGroup,
deleteUserGroup,
getUserGroupList,
getNodeGroupList,
updateUserGroup,
} from "@workspace/ui/services/admin/group";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query";
import { toast } from "sonner";
import UserGroupForm from "./user-group-form";
export default function UserGroups() {
const { t } = useTranslation("group");
const [loading, setLoading] = useState(false);
const ref = useRef<ProTableActions>(null);
const { data: nodeGroupsData } = useQuery({
queryKey: ["nodeGroups"],
queryFn: async () => {
const { data } = await getNodeGroupList({ page: 1, size: 1000 });
return data.data?.list || [];
},
});
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>{t("userGroups", "User Groups")}</CardTitle>
<CardDescription>
{t("userGroupsDescription", "Manage user groups for node access control")}
</CardDescription>
</CardHeader>
<CardContent>
<ProTable<API.UserGroup, API.GetUserGroupListRequest>
action={ref}
request={async (params) => {
const { data } = await getUserGroupList({
page: params.page || 1,
size: params.size || 10,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
columns={[
{
id: "id",
accessorKey: "id",
header: t("id", "ID"),
cell: ({ row }: { row: any }) => <span className="text-muted-foreground">#{row.getValue("id")}</span>,
},
{
id: "name",
accessorKey: "name",
header: t("name", "Name"),
},
{
id: "for_calculation",
accessorKey: "for_calculation",
header: t("forCalculation", "For Calculation"),
cell: ({ row }: { row: any }) => {
const forCalculation = row.getValue("for_calculation");
return forCalculation ? (
<Badge variant="default">{t("yes", "Yes")}</Badge>
) : (
<Badge variant="secondary">{t("no", "No")}</Badge>
);
},
},
{
id: "description",
accessorKey: "description",
header: t("description", "Description"),
cell: ({ row }: { row: any }) => row.getValue("description") || "--",
},
{
id: "node_group_id",
accessorKey: "node_group_id",
header: t("nodeGroup", "Node Group"),
cell: ({ row }: { row: any }) => {
const nodeGroupId = row.getValue("node_group_id");
const group = nodeGroupsData?.find((g) => g.id === nodeGroupId);
return group?.name || "--";
},
},
{
id: "sort",
accessorKey: "sort",
header: t("sort", "Sort"),
},
]}
actions={{
render: (row: any) => [
<UserGroupForm
key={`edit-${row.id}`}
initialValues={row}
loading={loading}
nodeGroups={nodeGroupsData || []}
onSubmit={async (values) => {
setLoading(true);
try {
await updateUserGroup({
id: row.id,
...values,
} as unknown as API.UpdateUserGroupRequest);
toast.success(t("updated", "Updated successfully"));
ref.current?.refresh();
setLoading(false);
return true;
} catch {
setLoading(false);
return false;
}
}}
title={t("editUserGroup", "Edit User Group")}
trigger={
<Button variant="outline" size="sm">
{t("edit", "Edit")}
</Button>
}
/>,
<ConfirmButton
key="delete"
cancelText={t("cancel", "Cancel")}
confirmText={t("confirm", "Confirm")}
description={t(
"deleteUserGroupConfirm",
"This will delete the user group. Users in this group will be reassigned to the default group."
)}
onConfirm={async () => {
await deleteUserGroup({ id: row.id });
toast.success(t("deleted", "Deleted successfully"));
ref.current?.refresh();
setLoading(false);
}}
title={t("confirmDelete", "Confirm Delete")}
trigger={
<Button variant="destructive" size="sm">
{t("delete", "Delete")}
</Button>
}
/>,
],
}}
header={{
title: t("userGroups", "User Groups"),
toolbar: (
<UserGroupForm
key="create"
initialValues={undefined}
loading={loading}
nodeGroups={nodeGroupsData || []}
onSubmit={async (values) => {
setLoading(true);
try {
await createUserGroup(values as API.CreateUserGroupRequest);
toast.success(t("created", "Created successfully"));
ref.current?.refresh();
setLoading(false);
return true;
} catch {
setLoading(false);
return false;
}
}}
title={t("createUserGroup", "Create User Group")}
trigger={
<Button>
{t("create", "Create")}
</Button>
}
/>
),
}}
/>
</CardContent>
</Card>
</div>
);
}

View File

@ -1,215 +0,0 @@
"use client";
import { Button } from "@workspace/ui/components/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@workspace/ui/components/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select";
import { Label } from "@workspace/ui/components/label";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Badge } from "@workspace/ui/components/badge";
import { AlertTriangle } from "lucide-react";
import {
getSubscribeMapping,
getUserGroupList,
migrateUsersToGroup,
} from "@workspace/ui/services/admin/group";
interface MigrateUsersDialogProps {
subscribeId: number;
subscribeName: string;
}
export default function MigrateUsersDialog({
subscribeId,
subscribeName,
}: MigrateUsersDialogProps) {
const { t } = useTranslation("product");
const [open, setOpen] = useState(false);
const [selectedGroupId, setSelectedGroupId] = useState<number>(0);
const [migrating, setMigrating] = useState(false);
// Fetch subscribe mapping to get current user group
const { data: mappingsData, isLoading: mappingsLoading } = useQuery({
enabled: open,
queryKey: ["subscribeMapping"],
queryFn: async () => {
const { data } = await getSubscribeMapping({ page: 1, size: 1000 });
return data.data;
},
});
// Fetch user groups for the dropdown
const { data: userGroupsData } = useQuery({
enabled: open,
queryKey: ["userGroups"],
queryFn: async () => {
const { data } = await getUserGroupList({ page: 1, size: 1000 });
return data.data?.list || [];
},
});
// Find the current mapping for this subscribe
const currentMapping = mappingsData?.list?.find(
(m) => m.subscribe_id === subscribeId
);
const currentGroupId = currentMapping?.user_group_id;
const currentUserGroup = currentGroupId
? userGroupsData?.find((g) => g.id === currentGroupId)
: undefined;
const handleMigrate = async () => {
if (!selectedGroupId) {
toast.error(t("selectTargetGroupFirst", "Please select a target group first"));
return;
}
if (selectedGroupId === currentGroupId) {
toast.error(t("cannotMigrateToSameGroup", "Cannot migrate to the same group"));
return;
}
setMigrating(true);
try {
await migrateUsersToGroup({
from_user_group_id: currentGroupId!,
to_user_group_id: selectedGroupId,
include_locked: false,
});
toast.success(t("migrateUsersSuccess", "Successfully migrated users to the target group"));
setOpen(false);
} catch (error) {
console.error("Failed to migrate users:", error);
toast.error(t("migrateUsersFailed", "Failed to migrate users"));
} finally {
setMigrating(false);
}
};
const availableGroups = userGroupsData?.filter(
(g) => !currentGroupId || g.id !== currentGroupId
);
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>
<Button variant="outline">{t("migrateUsers", "Migrate Users")}</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t("migrateUsersTitle", "Migrate Users")} - {subscribeName}</DialogTitle>
<DialogDescription>
{t(
"migrateUsersDescription",
"Migrate all users from the current user group to another group"
)}
</DialogDescription>
</DialogHeader>
{mappingsLoading ? (
<div className="py-8 text-center text-muted-foreground">
{t("loading", "Loading...")}
</div>
) : (
<div className="space-y-4 py-4">
{/* Current User Group Info */}
<div className="rounded-lg border p-4 space-y-2">
<Label>{t("currentUserGroup", "Current User Group")}:</Label>
<div className="flex items-center gap-2">
{currentUserGroup ? (
<>
<Badge variant="outline">{currentUserGroup.name}</Badge>
</>
) : (
<span className="text-muted-foreground">
{t("noMapping", "No mapping set")}
</span>
)}
</div>
</div>
{/* Warning Message */}
{currentUserGroup && (
<div className="flex items-start gap-2 rounded-md bg-yellow-50 p-3 text-sm text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400">
<AlertTriangle className="h-4 w-4 mt-0.5 flex-shrink-0" />
<p>
{t("migrateUsersWarning", "This will migrate users from \"{group}\" to the target group. This action cannot be undone.")
.replace("{group}", currentUserGroup.name || "")
}
</p>
</div>
)}
{/* Target User Group Selection */}
<div className="space-y-2">
<Label htmlFor="target-group">{t("targetUserGroup", "Target User Group")}:</Label>
<Select
value={selectedGroupId?.toString() || ""}
onValueChange={(val) => setSelectedGroupId(Number(val))}
disabled={!currentGroupId}
>
<SelectTrigger id="target-group">
<SelectValue
placeholder={
currentGroupId
? t("selectTargetGroup", "Select a target group...")
: t("noSourceGroup", "No source group available")
}
/>
</SelectTrigger>
<SelectContent>
{availableGroups?.map((group) => (
<SelectItem key={group.id} value={String(group.id)}>
{group.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Selected Target Group Info */}
{selectedGroupId && (
<div className="rounded-md bg-muted p-3">
<span className="text-sm font-medium">{t("selectedGroup", "Selected Group")}: </span>
<Badge variant="secondary">
{availableGroups?.find((g) => g.id === selectedGroupId)?.name}
</Badge>
</div>
)}
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
{t("cancel", "Cancel")}
</Button>
<Button
onClick={handleMigrate}
disabled={!selectedGroupId || !currentGroupId || migrating}
>
{migrating ? t("migrating", "Migrating...") : t("confirm", "Confirm")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -1,199 +0,0 @@
"use client";
import { Button } from "@workspace/ui/components/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@workspace/ui/components/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@workspace/ui/components/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select";
import {
getUserGroupList,
} from "@workspace/ui/services/admin/group";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { useQuery } from "@tanstack/react-query";
import React, { useEffect } from "react";
const editUserGroupSchema = z.object({
user_group_id: z.number().min(0),
group_locked: z.boolean(),
});
type EditUserGroupFormValues = z.infer<typeof editUserGroupSchema>;
interface EditUserGroupDialogProps {
userId: number;
userSubscribeId?: number;
currentGroupId?: number | undefined;
currentLocked?: boolean | undefined;
currentGroupIds?: number[] | null | undefined;
trigger: React.ReactNode;
onSubmit?: (values: EditUserGroupFormValues) => Promise<boolean>;
}
export default function EditUserGroupDialog({
userId: _userId,
userSubscribeId: _userSubscribeId,
currentGroupId,
currentLocked,
currentGroupIds,
trigger,
onSubmit,
}: EditUserGroupDialogProps) {
const { t } = useTranslation("user");
const [open, setOpen] = React.useState(false);
// Fetch user groups list
const { data: groupsData } = useQuery({
enabled: open,
queryKey: ["getUserGroupList"],
queryFn: async () => {
const { data } = await getUserGroupList({
page: 1,
size: 100,
});
return data.data?.list || [];
},
});
const form = useForm<EditUserGroupFormValues>({
resolver: zodResolver(editUserGroupSchema),
defaultValues: {
user_group_id: 0,
group_locked: false,
},
});
// Reset form when dialog closes
useEffect(() => {
if (!open) {
form.reset();
}
}, [open, form]);
// Set form values when dialog opens
useEffect(() => {
if (open) {
// Support both usage scenarios:
// 1. User list page: currentGroupId (single number)
// 2. Subscribe detail page: currentGroupIds (array)
const groupId = currentGroupId || (currentGroupIds?.[0]) || 0;
form.reset({
user_group_id: groupId,
group_locked: currentLocked || false,
});
}
}, [open, currentGroupId, currentGroupIds, currentLocked, form]);
const handleSubmit = async (values: EditUserGroupFormValues) => {
if (onSubmit) {
const success = await onSubmit(values);
if (success) {
setOpen(false);
}
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{t("editUserGroup", "Edit User Group")}</DialogTitle>
<DialogDescription>
{t(
"editUserGroupDescription",
"Edit user group assignment and lock status"
)}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
name="user_group_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("userGroup", "User Group")}</FormLabel>
<Select
value={field.value > 0 ? String(field.value) : undefined}
onValueChange={(value) => field.onChange(parseInt(value))}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectGroup", "Select a group")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{groupsData?.map((group: API.UserGroup) => (
<SelectItem key={group.id} value={String(group.id)}>
{group.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="group_locked"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>{t("lockGroup", "Lock Group")}</FormLabel>
<div className="text-[0.8rem] text-muted-foreground">
{t(
"lockGroupDescription",
"Prevent automatic grouping from changing this user's group"
)}
</div>
</div>
<FormControl>
<input
type="checkbox"
checked={field.value}
onChange={(e) => field.onChange(e.target.checked)}
className="h-4 w-4"
/>
</FormControl>
</FormItem>
)}
/>
<DialogFooter>
<Button type="submit">
{t("save", "Save")}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}