+
+
+
+
+
+
+
+
+
+ updatePartialSetting({ theme: value })}
+ className="min-w-fit"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ updatePartialSetting({ disallowUserRegistration: checked })}
+ />
+
+
+
+ updatePartialSetting({ disallowPasswordAuth: checked })}
+ />
+
+
+
+ updatePartialSetting({ disallowChangeUsername: checked })}
+ />
+
+
+
+ updatePartialSetting({ disallowChangeNickname: checked })}
+ />
+
+
+
+
+
+
+
+
@@ -173,7 +175,7 @@ const InstanceSection = observer(() => {
toast.success("Profile updated successfully!");
}}
/>
-
+
);
});
diff --git a/web/src/components/Settings/MemberSection.tsx b/web/src/components/Settings/MemberSection.tsx
index f6ee2137e..2f4177625 100644
--- a/web/src/components/Settings/MemberSection.tsx
+++ b/web/src/components/Settings/MemberSection.tsx
@@ -14,6 +14,8 @@ import { User, User_Role } from "@/types/proto/api/v1/user_service";
import { useTranslate } from "@/utils/i18n";
import CreateUserDialog from "../CreateUserDialog";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu";
+import SettingSection from "./SettingSection";
+import SettingTable from "./SettingTable";
const MemberSection = observer(() => {
const t = useTranslate();
@@ -101,84 +103,79 @@ const MemberSection = observer(() => {
};
return (
-
-
-
{t("setting.member-section.create-a-member")}
+
{t("common.create")}
-
-
-
{t("setting.member-list")}
-
-
-
-
-
-
- |
- {t("common.username")}
- |
-
- {t("common.role")}
- |
-
- {t("common.nickname")}
- |
-
- {t("common.email")}
- |
- |
-
-
-
- {sortedUsers.map((user) => (
-
- |
- {user.username}
- {user.state === State.ARCHIVED && "(Archived)"}
- |
- {stringifyUserRole(user.role)} |
- {user.displayName} |
- {user.email} |
-
- {currentUser?.name === user.name ? (
- {t("common.yourself")}
+ }
+ >
+ (
+
+ {user.username}
+ {user.state === State.ARCHIVED && (Archived)}
+
+ ),
+ },
+ {
+ key: "role",
+ header: t("common.role"),
+ render: (_, user: User) => stringifyUserRole(user.role),
+ },
+ {
+ key: "displayName",
+ header: t("common.nickname"),
+ render: (_, user: User) => user.displayName,
+ },
+ {
+ key: "email",
+ header: t("common.email"),
+ render: (_, user: User) => user.email,
+ },
+ {
+ key: "actions",
+ header: "",
+ className: "text-right",
+ render: (_, user: User) =>
+ currentUser?.name === user.name ? (
+ {t("common.yourself")}
+ ) : (
+
+
+
+
+
+ handleEditUser(user)}>{t("common.update")}
+ {user.state === State.NORMAL ? (
+ handleArchiveUserClick(user)}>
+ {t("setting.member-section.archive-member")}
+
) : (
-
-
-
-
-
- handleEditUser(user)}>{t("common.update")}
- {user.state === State.NORMAL ? (
- handleArchiveUserClick(user)}>
- {t("setting.member-section.archive-member")}
-
- ) : (
- <>
- handleRestoreUserClick(user)}>{t("common.restore")}
- handleDeleteUserClick(user)}
- className="text-destructive focus:text-destructive"
- >
- {t("setting.member-section.delete-member")}
-
- >
- )}
-
-
+ <>
+ handleRestoreUserClick(user)}>{t("common.restore")}
+ handleDeleteUserClick(user)} className="text-destructive focus:text-destructive">
+ {t("setting.member-section.delete-member")}
+
+ >
)}
- |
-
- ))}
-
-
-
-
+
+
+ ),
+ },
+ ]}
+ data={sortedUsers}
+ emptyMessage="No members found"
+ getRowKey={(user) => user.name}
+ />
{/* Create User Dialog */}
@@ -207,7 +204,7 @@ const MemberSection = observer(() => {
onConfirm={confirmDeleteUser}
confirmVariant="destructive"
/>
-
+
);
});
diff --git a/web/src/components/Settings/MemoRelatedSettings.tsx b/web/src/components/Settings/MemoRelatedSettings.tsx
index dbbf68101..c7abba930 100644
--- a/web/src/components/Settings/MemoRelatedSettings.tsx
+++ b/web/src/components/Settings/MemoRelatedSettings.tsx
@@ -11,6 +11,9 @@ import { instanceStore } from "@/store";
import { instanceSettingNamePrefix } from "@/store/common";
import { InstanceSetting_MemoRelatedSetting, InstanceSetting_Key } from "@/types/proto/api/v1/instance_service";
import { useTranslate } from "@/utils/i18n";
+import SettingGroup from "./SettingGroup";
+import SettingRow from "./SettingRow";
+import SettingSection from "./SettingSection";
const MemoRelatedSettings = observer(() => {
const t = useTranslate();
@@ -65,122 +68,125 @@ const MemoRelatedSettings = observer(() => {
};
return (
-
-
{t("setting.memo-related-settings.title")}
-
- {t("setting.system-section.disable-public-memos")}
- updatePartialSetting({ disallowPublicVisibility: checked })}
- />
-
-
- {t("setting.system-section.display-with-updated-time")}
- updatePartialSetting({ displayWithUpdateTime: checked })}
- />
-
-
- {t("setting.memo-related-settings.enable-link-preview")}
- updatePartialSetting({ enableLinkPreview: checked })}
- />
-
-
- {t("setting.system-section.enable-double-click-to-edit")}
- updatePartialSetting({ enableDoubleClickEdit: checked })}
- />
-
-
- {t("setting.system-section.disable-markdown-shortcuts-in-editor")}
- updatePartialSetting({ disableMarkdownShortcuts: checked })}
- />
-
-
- {t("setting.memo-related-settings.content-lenght-limit")}
- updatePartialSetting({ contentLengthLimit: Number(event.target.value) })}
- />
-
-
-
{t("setting.memo-related-settings.reactions")}
-
- {memoRelatedSetting.reactions.map((reactionType) => {
- return (
-
- {reactionType}
- updatePartialSetting({ reactions: memoRelatedSetting.reactions.filter((r) => r !== reactionType) })}
- >
-
-
-
- );
- })}
-
-
-
- {t("setting.memo-related-settings.enable-blur-nsfw-content")}
+
+
+
+
updatePartialSetting({ enableBlurNsfwContent: checked })}
/>
-
-
- {memoRelatedSetting.nsfwTags.map((nsfwTag) => {
- return (
-
+
+
+
+
NSFW Tags
+
+ {memoRelatedSetting.nsfwTags.map((nsfwTag) => (
+
{nsfwTag}
updatePartialSetting({ nsfwTags: memoRelatedSetting.nsfwTags.filter((r) => r !== nsfwTag) })}
>
-
+
- );
- })}
-
-
-
+
+
+
-
+
);
});
diff --git a/web/src/components/Settings/MyAccountSection.tsx b/web/src/components/Settings/MyAccountSection.tsx
index a9fce54a2..3bd7add0b 100644
--- a/web/src/components/Settings/MyAccountSection.tsx
+++ b/web/src/components/Settings/MyAccountSection.tsx
@@ -8,6 +8,8 @@ import UpdateAccountDialog from "../UpdateAccountDialog";
import UserAvatar from "../UserAvatar";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu";
import AccessTokenSection from "./AccessTokenSection";
+import SettingGroup from "./SettingGroup";
+import SettingSection from "./SettingSection";
import UserSessionsSection from "./UserSessionsSection";
const MyAccountSection = () => {
@@ -25,44 +27,50 @@ const MyAccountSection = () => {
};
return (
-
-
{t("setting.account-section.title")}
-
-
-
-
- {user.displayName}
- ({user.username})
-
-
{user.description}
-
-
-
-
-
-
-
+
-
-
+
+
+
+
+
+
+
{/* Update Account Dialog */}
{/* Change Password Dialog */}
-
+
);
};
diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx
index 3d1353d75..65cf41f78 100644
--- a/web/src/components/Settings/PreferencesSection.tsx
+++ b/web/src/components/Settings/PreferencesSection.tsx
@@ -1,6 +1,5 @@
import { observer } from "mobx-react-lite";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
-import { Separator } from "@/components/ui/separator";
import { userStore, instanceStore } from "@/store";
import { Visibility } from "@/types/proto/api/v1/memo_service";
import { UserSetting_GeneralSetting } from "@/types/proto/api/v1/user_service";
@@ -9,6 +8,9 @@ import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/
import LocaleSelect from "../LocaleSelect";
import ThemeSelect from "../ThemeSelect";
import VisibilityIcon from "../VisibilityIcon";
+import SettingGroup from "./SettingGroup";
+import SettingRow from "./SettingRow";
+import SettingSection from "./SettingSection";
import WebhookSection from "./WebhookSection";
const PreferencesSection = observer(() => {
@@ -41,46 +43,43 @@ const PreferencesSection = observer(() => {
};
return (
-
-
{t("common.basic")}
+
+
+
+
+
-
- {t("common.language")}
-
-
+
+
+
+
-
- {t("setting.preference-section.theme")}
-
-
+
+
+
+
+
- {t("setting.preference")}
-
-
-
{t("setting.preference-section.default-memo-visibility")}
-
-
-
-
-
-
-
+
+
+
+
);
});
diff --git a/web/src/components/Settings/SSOSection.tsx b/web/src/components/Settings/SSOSection.tsx
index 8cc31690c..b50a6ad12 100644
--- a/web/src/components/Settings/SSOSection.tsx
+++ b/web/src/components/Settings/SSOSection.tsx
@@ -1,15 +1,16 @@
-import { MoreVerticalIcon } from "lucide-react";
+import { MoreVerticalIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import ConfirmDialog from "@/components/ConfirmDialog";
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
-import { Separator } from "@/components/ui/separator";
import { identityProviderServiceClient } from "@/grpcweb";
import { IdentityProvider } from "@/types/proto/api/v1/idp_service";
import { useTranslate } from "@/utils/i18n";
import CreateIdentityProviderDialog from "../CreateIdentityProviderDialog";
import LearnMore from "../LearnMore";
+import SettingSection from "./SettingSection";
+import SettingTable from "./SettingTable";
const SSOSection = () => {
const t = useTranslate();
@@ -68,48 +69,60 @@ const SSOSection = () => {
};
return (
-
-
-
- {t("setting.sso-section.sso-list")}
+
+ {t("setting.sso-section.sso-list")}
-
+ }
+ actions={
+
+
{t("common.create")}
-
-
- {identityProviderList.map((identityProvider) => (
-
-
-
- {identityProvider.title}
- ({identityProvider.type})
-
-
-
-
-
-
-
-
-
-
- handleEditIdentityProvider(identityProvider)}>{t("common.edit")}
- handleDeleteIdentityProvider(identityProvider)}>{t("common.delete")}
-
-
-
-
- ))}
- {identityProviderList.length === 0 && (
-
-
{t("setting.sso-section.no-sso-found")}
-
- )}
+ }
+ >
+
(
+
+ {provider.title}
+ ({provider.type})
+
+ ),
+ },
+ {
+ key: "actions",
+ header: "",
+ className: "text-right",
+ render: (_, provider: IdentityProvider) => (
+
+
+
+
+
+
+
+ handleEditIdentityProvider(provider)}>{t("common.edit")}
+ handleDeleteIdentityProvider(provider)}
+ className="text-destructive focus:text-destructive"
+ >
+ {t("common.delete")}
+
+
+
+ ),
+ },
+ ]}
+ data={identityProviderList}
+ emptyMessage={t("setting.sso-section.no-sso-found")}
+ getRowKey={(provider) => provider.name}
+ />
{
onConfirm={confirmDeleteIdentityProvider}
confirmVariant="destructive"
/>
-
+
);
};
diff --git a/web/src/components/Settings/SettingGroup.tsx b/web/src/components/Settings/SettingGroup.tsx
new file mode 100644
index 000000000..7aac58c1c
--- /dev/null
+++ b/web/src/components/Settings/SettingGroup.tsx
@@ -0,0 +1,34 @@
+import React from "react";
+import { Separator } from "@/components/ui/separator";
+import { cn } from "@/lib/utils";
+
+interface SettingGroupProps {
+ title?: string;
+ description?: string;
+ children: React.ReactNode;
+ className?: string;
+ showSeparator?: boolean;
+}
+
+/**
+ * Groups related settings together with optional title and separator
+ * Use this to organize multiple SettingRows under a common category
+ */
+const SettingGroup: React.FC = ({ title, description, children, className, showSeparator = false }) => {
+ return (
+ <>
+ {showSeparator && }
+
+ {(title || description) && (
+
+ {title &&
{title}
}
+ {description &&
{description}
}
+
+ )}
+
{children}
+
+ >
+ );
+};
+
+export default SettingGroup;
diff --git a/web/src/components/Settings/SettingRow.tsx b/web/src/components/Settings/SettingRow.tsx
new file mode 100644
index 000000000..f71f50c0f
--- /dev/null
+++ b/web/src/components/Settings/SettingRow.tsx
@@ -0,0 +1,45 @@
+import { HelpCircleIcon } from "lucide-react";
+import React from "react";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import { cn } from "@/lib/utils";
+
+interface SettingRowProps {
+ label: string;
+ description?: string;
+ tooltip?: string;
+ children: React.ReactNode;
+ className?: string;
+ vertical?: boolean;
+}
+
+/**
+ * Standardized row component for individual settings
+ * Provides consistent label/control layout with optional tooltip
+ */
+const SettingRow: React.FC = ({ label, description, tooltip, children, className, vertical = false }) => {
+ return (
+
+
+
+
{label}
+ {tooltip && (
+
+
+
+
+
+
+ {tooltip}
+
+
+
+ )}
+
+ {description &&
{description}
}
+
+
{children}
+
+ );
+};
+
+export default SettingRow;
diff --git a/web/src/components/Settings/SettingSection.tsx b/web/src/components/Settings/SettingSection.tsx
new file mode 100644
index 000000000..ec3a35f5a
--- /dev/null
+++ b/web/src/components/Settings/SettingSection.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import { cn } from "@/lib/utils";
+
+interface SettingSectionProps {
+ title?: React.ReactNode;
+ description?: string;
+ children: React.ReactNode;
+ className?: string;
+ actions?: React.ReactNode;
+}
+
+/**
+ * Wrapper component for consistent section layout in settings pages
+ * Provides standardized spacing, titles, and descriptions
+ */
+const SettingSection: React.FC = ({ title, description, children, className, actions }) => {
+ return (
+
+ {(title || description || actions) && (
+
+
+ {title && (
+
{typeof title === "string" ?
{title}
: title}
+ )}
+ {description &&
{description}
}
+
+ {actions &&
{actions}
}
+
+ )}
+
{children}
+
+ );
+};
+
+export default SettingSection;
diff --git a/web/src/components/Settings/SettingTable.tsx b/web/src/components/Settings/SettingTable.tsx
new file mode 100644
index 000000000..26367a466
--- /dev/null
+++ b/web/src/components/Settings/SettingTable.tsx
@@ -0,0 +1,69 @@
+import React from "react";
+import { cn } from "@/lib/utils";
+
+interface SettingTableColumn {
+ key: string;
+ header: string;
+ className?: string;
+ render?: (value: any, row: any) => React.ReactNode;
+}
+
+interface SettingTableProps {
+ columns: SettingTableColumn[];
+ data: any[];
+ emptyMessage?: string;
+ className?: string;
+ getRowKey?: (row: any, index: number) => string;
+}
+
+/**
+ * Standardized table component for settings data lists
+ * Provides consistent styling for tables in settings pages
+ */
+const SettingTable: React.FC = ({ columns, data, emptyMessage = "No data", className, getRowKey }) => {
+ return (
+
+
+
+
+
+ {columns.map((column) => (
+ |
+ {column.header}
+ |
+ ))}
+
+
+
+ {data.length === 0 ? (
+
+ |
+ {emptyMessage}
+ |
+
+ ) : (
+ data.map((row, rowIndex) => {
+ const rowKey = getRowKey ? getRowKey(row, rowIndex) : rowIndex.toString();
+ return (
+
+ {columns.map((column) => {
+ const value = row[column.key];
+ const content = column.render ? column.render(value, row) : value;
+ return (
+ |
+ {content}
+ |
+ );
+ })}
+
+ );
+ })
+ )}
+
+
+
+
+ );
+};
+
+export default SettingTable;
diff --git a/web/src/components/Settings/StorageSection.tsx b/web/src/components/Settings/StorageSection.tsx
index 5dfa42def..3233d1010 100644
--- a/web/src/components/Settings/StorageSection.tsx
+++ b/web/src/components/Settings/StorageSection.tsx
@@ -1,5 +1,4 @@
import { isEqual } from "lodash-es";
-import { HelpCircleIcon } from "lucide-react";
import { observer } from "mobx-react-lite";
import React, { useEffect, useMemo, useState } from "react";
import { toast } from "react-hot-toast";
@@ -8,7 +7,6 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { instanceStore } from "@/store";
import { instanceSettingNamePrefix } from "@/store/common";
import {
@@ -18,6 +16,9 @@ import {
InstanceSetting_StorageSetting_StorageType,
} from "@/types/proto/api/v1/instance_service";
import { useTranslate } from "@/utils/i18n";
+import SettingGroup from "./SettingGroup";
+import SettingRow from "./SettingRow";
+import SettingSection from "./SettingSection";
const StorageSection = observer(() => {
const t = useTranslate();
@@ -131,107 +132,89 @@ const StorageSection = observer(() => {
};
return (
-
-
{t("setting.storage-section.current-storage")}
-
{
- handleStorageTypeChanged(value as InstanceSetting_StorageSetting_StorageType);
- }}
- className="flex flex-row gap-4"
- >
-
-
-
+
+
+
+
{
+ handleStorageTypeChanged(value as InstanceSetting_StorageSetting_StorageType);
+ }}
+ className="flex flex-row gap-4"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
{t("setting.system-section.max-upload-size")}
-
-
-
-
-
-
- {t("setting.system-section.max-upload-size-hint")}
-
-
-
-
-
-
- {instanceStorageSetting.storageType !== InstanceSetting_StorageSetting_StorageType.DATABASE && (
-
- {t("setting.storage-section.filepath-template")}
-
-
- )}
- {instanceStorageSetting.storageType === InstanceSetting_StorageSetting_StorageType.S3 && (
- <>
-
- Access key id
+
+
+
+
+
+ {instanceStorageSetting.storageType !== InstanceSetting_StorageSetting_StorageType.DATABASE && (
+
-
-
- Access key secret
+
+ )}
+
+
+ {instanceStorageSetting.storageType === InstanceSetting_StorageSetting_StorageType.S3 && (
+
+
+
+
+
+
-
-
- Endpoint
-
-
-
- Region
-
-
-
- Bucket
-
-
-
- Use Path Style
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
handleS3ConfigUsePathStyleChanged({ target: { checked } } as any)}
/>
-
- >
+
+
)}
-
+
);
});
diff --git a/web/src/components/Settings/UserSessionsSection.tsx b/web/src/components/Settings/UserSessionsSection.tsx
index 292e09140..c4bb76f4e 100644
--- a/web/src/components/Settings/UserSessionsSection.tsx
+++ b/web/src/components/Settings/UserSessionsSection.tsx
@@ -7,6 +7,7 @@ import { userServiceClient } from "@/grpcweb";
import useCurrentUser from "@/hooks/useCurrentUser";
import { UserSession } from "@/types/proto/api/v1/user_service";
import { useTranslate } from "@/utils/i18n";
+import SettingTable from "./SettingTable";
const listUserSessions = async (parent: string) => {
const { sessions } = await userServiceClient.listUserSessions({ parent });
@@ -71,104 +72,87 @@ const UserSessionsSection = () => {
};
return (
-
-
-
-
-
- {t("setting.user-sessions-section.title")}
-
-
{t("setting.user-sessions-section.description")}
-
-
-
-
-
-
-
-
- |
- {t("setting.user-sessions-section.device")}
- |
-
- {t("setting.user-sessions-section.last-active")}
- |
-
- {t("common.delete")}
- |
-
-
-
- {userSessions.map((userSession) => (
-
-
-
- {getDeviceIcon(userSession.clientInfo?.deviceType || "")}
-
-
- {formatDeviceInfo(userSession.clientInfo)}
- {isCurrentSession(userSession) && (
-
-
- {t("setting.user-sessions-section.current")}
-
- )}
-
- {getFormattedSessionId(userSession.sessionId)}
-
-
- |
-
-
-
- {userSession.lastAccessedTime?.toLocaleString()}
-
- |
-
- {
- handleRevokeSession(userSession);
- }}
- title={
- isCurrentSession(userSession)
- ? t("setting.user-sessions-section.cannot-revoke-current")
- : t("setting.user-sessions-section.revoke-session")
- }
- >
-
-
- |
-
- ))}
-
-
- {userSessions.length === 0 && (
-
{t("setting.user-sessions-section.no-sessions")}
- )}
-
-
-
-
!open && setRevokeTarget(undefined)}
- title={
- revokeTarget
- ? t("setting.user-sessions-section.session-revocation", {
- sessionId: getFormattedSessionId(revokeTarget.sessionId),
- })
- : ""
- }
- description={revokeTarget ? t("setting.user-sessions-section.session-revocation-description") : ""}
- confirmLabel={t("setting.user-sessions-section.revoke-session-button")}
- cancelLabel={t("common.cancel")}
- onConfirm={confirmRevokeSession}
- confirmVariant="destructive"
- />
+
+
+
{t("setting.user-sessions-section.title")}
+
{t("setting.user-sessions-section.description")}
+
+
(
+
+ {getDeviceIcon(session.clientInfo?.deviceType || "")}
+
+
+ {formatDeviceInfo(session.clientInfo)}
+ {isCurrentSession(session) && (
+
+
+ {t("setting.user-sessions-section.current")}
+
+ )}
+
+ {getFormattedSessionId(session.sessionId)}
+
+
+ ),
+ },
+ {
+ key: "lastAccessedTime",
+ header: t("setting.user-sessions-section.last-active"),
+ render: (_, session: UserSession) => (
+
+
+ {session.lastAccessedTime?.toLocaleString()}
+
+ ),
+ },
+ {
+ key: "actions",
+ header: "",
+ className: "text-right",
+ render: (_, session: UserSession) => (
+ handleRevokeSession(session)}
+ title={
+ isCurrentSession(session)
+ ? t("setting.user-sessions-section.cannot-revoke-current")
+ : t("setting.user-sessions-section.revoke-session")
+ }
+ >
+
+
+ ),
+ },
+ ]}
+ data={userSessions}
+ emptyMessage={t("setting.user-sessions-section.no-sessions")}
+ getRowKey={(session) => session.sessionId}
+ />
+
+ !open && setRevokeTarget(undefined)}
+ title={
+ revokeTarget
+ ? t("setting.user-sessions-section.session-revocation", {
+ sessionId: getFormattedSessionId(revokeTarget.sessionId),
+ })
+ : ""
+ }
+ description={revokeTarget ? t("setting.user-sessions-section.session-revocation-description") : ""}
+ confirmLabel={t("setting.user-sessions-section.revoke-session-button")}
+ cancelLabel={t("common.cancel")}
+ onConfirm={confirmRevokeSession}
+ confirmVariant="destructive"
+ />
);
};
diff --git a/web/src/components/Settings/WebhookSection.tsx b/web/src/components/Settings/WebhookSection.tsx
index 817db5c99..3838cf173 100644
--- a/web/src/components/Settings/WebhookSection.tsx
+++ b/web/src/components/Settings/WebhookSection.tsx
@@ -1,4 +1,4 @@
-import { ExternalLinkIcon, TrashIcon } from "lucide-react";
+import { ExternalLinkIcon, PlusIcon, TrashIcon } from "lucide-react";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { Link } from "react-router-dom";
@@ -9,6 +9,7 @@ import useCurrentUser from "@/hooks/useCurrentUser";
import { UserWebhook } from "@/types/proto/api/v1/user_service";
import { useTranslate } from "@/utils/i18n";
import CreateWebhookDialog from "../CreateWebhookDialog";
+import SettingTable from "./SettingTable";
const WebhookSection = () => {
const t = useTranslate();
@@ -52,71 +53,58 @@ const WebhookSection = () => {
};
return (
-
-
-
-
{t("setting.webhook-section.title")}
-
-
- setIsCreateWebhookDialogOpen(true)}>
- {t("common.create")}
-
-
+
+
+
{t("setting.webhook-section.title")}
+
setIsCreateWebhookDialogOpen(true)} size="sm">
+
+ {t("common.create")}
+
-
-
-
-
-
-
- |
- {t("common.name")}
- |
-
- {t("setting.webhook-section.url")}
- |
-
- {t("common.delete")}
- |
-
-
-
- {webhooks.map((webhook) => (
-
- | {webhook.displayName} |
-
- {webhook.url}
- |
-
- handleDeleteWebhook(webhook)}>
-
-
- |
-
- ))}
- {webhooks.length === 0 && (
-
- |
- {t("setting.webhook-section.no-webhooks-found")}
- |
-
- )}
-
-
-
-
-
-
+
{webhook.displayName},
+ },
+ {
+ key: "url",
+ header: t("setting.webhook-section.url"),
+ render: (_, webhook: UserWebhook) => (
+
+ {webhook.url}
+
+ ),
+ },
+ {
+ key: "actions",
+ header: "",
+ className: "text-right",
+ render: (_, webhook: UserWebhook) => (
+ handleDeleteWebhook(webhook)}>
+
+
+ ),
+ },
+ ]}
+ data={webhooks}
+ emptyMessage={t("setting.webhook-section.no-webhooks-found")}
+ getRowKey={(webhook) => webhook.name}
+ />
+
+
{t("common.learn-more")}
-
+
+