Commit 751a54e2 authored by ThinhNC's avatar ThinhNC

feat(system-config): add sync from .env button and update 7-day retention UI

parent be29c7a3
...@@ -13,7 +13,9 @@ import { ...@@ -13,7 +13,9 @@ import {
AlertCircle, AlertCircle,
RefreshCw, RefreshCw,
Lock, Lock,
FileCode,
} from "lucide-react"; } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useLanguage } from "@/providers/language-provider"; import { useLanguage } from "@/providers/language-provider";
...@@ -24,6 +26,7 @@ import { ...@@ -24,6 +26,7 @@ import {
useUpdateSystemConfig, useUpdateSystemConfig,
useToggleFeatureFlag, useToggleFeatureFlag,
useDeleteSystemConfig, useDeleteSystemConfig,
useSyncEnvConfigs,
} from "@/hooks/use-system-config"; } from "@/hooks/use-system-config";
import { import {
CreateSystemConfigDto, CreateSystemConfigDto,
...@@ -99,6 +102,7 @@ export function SystemConfigsManagementView({ ...@@ -99,6 +102,7 @@ export function SystemConfigsManagementView({
const updateMutation = useUpdateSystemConfig(); const updateMutation = useUpdateSystemConfig();
const toggleMutation = useToggleFeatureFlag(); const toggleMutation = useToggleFeatureFlag();
const deleteMutation = useDeleteSystemConfig(); const deleteMutation = useDeleteSystemConfig();
const syncEnvMutation = useSyncEnvConfigs();
// Sort toggle handler // Sort toggle handler
const handleToggleSort = (column: string) => { const handleToggleSort = (column: string) => {
...@@ -173,23 +177,53 @@ export function SystemConfigsManagementView({ ...@@ -173,23 +177,53 @@ export function SystemConfigsManagementView({
} }
}; };
const handleSyncEnv = async () => {
try {
const res = await syncEnvMutation.mutateAsync();
toast.success(
res?.syncedCount !== undefined
? `${t.systemConfigs.toast.syncEnvSuccess} (${res.syncedCount})`
: t.systemConfigs.toast.syncEnvSuccess,
);
} catch (err: unknown) {
const errorMsg =
err instanceof Error ? err.message : t.systemConfigs.toast.error;
toast.error(errorMsg);
}
};
const handleSubmitCreate = async (dto: CreateSystemConfigDto) => { const handleSubmitCreate = async (dto: CreateSystemConfigDto) => {
await createMutation.mutateAsync(dto); try {
setIsFormModalOpen(false); await createMutation.mutateAsync(dto);
toast.success(t.systemConfigs.toast.createSuccess);
setIsFormModalOpen(false);
} catch {
toast.error(t.systemConfigs.toast.error);
}
}; };
const handleSubmitUpdate = async ( const handleSubmitUpdate = async (
key: string, key: string,
dto: UpdateSystemConfigDto, dto: UpdateSystemConfigDto,
) => { ) => {
await updateMutation.mutateAsync({ key, dto }); try {
setIsFormModalOpen(false); await updateMutation.mutateAsync({ key, dto });
toast.success(t.systemConfigs.toast.updateSuccess);
setIsFormModalOpen(false);
} catch {
toast.error(t.systemConfigs.toast.error);
}
}; };
const handleConfirmDelete = async () => { const handleConfirmDelete = async () => {
if (!deletingConfig) return; if (!deletingConfig) return;
await deleteMutation.mutateAsync(deletingConfig.key); try {
setDeletingConfig(null); await deleteMutation.mutateAsync(deletingConfig.key);
toast.success(t.systemConfigs.toast.deleteSuccess);
setDeletingConfig(null);
} catch {
toast.error(t.systemConfigs.toast.error);
}
}; };
return ( return (
...@@ -211,13 +245,26 @@ export function SystemConfigsManagementView({ ...@@ -211,13 +245,26 @@ export function SystemConfigsManagementView({
</div> </div>
{canManage && ( {canManage && (
<Button <div className="flex items-center gap-2">
onClick={handleOpenCreate} <Button
className="rounded-2xl bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-bold px-4 py-2.5 shadow-md shadow-emerald-600/20 cursor-pointer transition-all hover:scale-[1.02] flex items-center gap-2" type="button"
> variant="outline"
<Plus className="h-4 w-4" /> onClick={handleSyncEnv}
<span>{t.systemConfigs.createBtn}</span> disabled={syncEnvMutation.isPending}
</Button> className="rounded-2xl border-emerald-500/30 hover:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 text-xs font-bold px-4 py-2.5 shadow-sm cursor-pointer transition-all hover:scale-[1.02] flex items-center gap-2"
>
<FileCode className={`h-4 w-4 ${syncEnvMutation.isPending ? "animate-spin" : ""}`} />
<span>{t.systemConfigs.syncEnvBtn}</span>
</Button>
<Button
type="button"
onClick={handleOpenCreate}
className="rounded-2xl bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-bold px-4 py-2.5 shadow-md shadow-emerald-600/20 cursor-pointer transition-all hover:scale-[1.02] flex items-center gap-2"
>
<Plus className="h-4 w-4" />
<span>{t.systemConfigs.createBtn}</span>
</Button>
</div>
)} )}
</div> </div>
)} )}
...@@ -315,6 +362,22 @@ export function SystemConfigsManagementView({ ...@@ -315,6 +362,22 @@ export function SystemConfigsManagementView({
</button> </button>
</div> </div>
{canManage && (
<button
onClick={handleSyncEnv}
disabled={syncEnvMutation.isPending}
className="flex items-center gap-1.5 h-9 px-3 rounded-2xl border border-emerald-500/30 bg-emerald-500/10 text-xs font-semibold text-emerald-600 dark:text-emerald-400 hover:bg-emerald-500/20 transition-all cursor-pointer disabled:opacity-50"
title={t.systemConfigs.syncEnvBtn}
>
<FileCode
className={`h-4 w-4 ${syncEnvMutation.isPending ? "animate-spin" : ""}`}
/>
<span className="hidden sm:inline">
{t.systemConfigs.syncEnvBtn}
</span>
</button>
)}
<button <button
onClick={() => refetch()} onClick={() => refetch()}
disabled={isFetching} disabled={isFetching}
......
...@@ -136,3 +136,19 @@ export function useDeleteSystemConfig() { ...@@ -136,3 +136,19 @@ export function useDeleteSystemConfig() {
}, },
}); });
} }
/**
* Hook đồng bộ toàn bộ giá trị cấu hình từ file .env
*/
export function useSyncEnvConfigs() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => systemConfigService.syncFromEnv(),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: SYSTEM_CONFIG_QUERY_KEYS.all,
});
},
});
}
...@@ -1348,6 +1348,7 @@ export const translations = { ...@@ -1348,6 +1348,7 @@ export const translations = {
editBtn: "Chỉnh sửa", editBtn: "Chỉnh sửa",
deleteBtn: "Xóa cấu hình", deleteBtn: "Xóa cấu hình",
refreshBtn: "Làm mới", refreshBtn: "Làm mới",
syncEnvBtn: "Đồng bộ từ .env",
table: { table: {
key: "Khóa cấu hình", key: "Khóa cấu hình",
value: "Giá trị", value: "Giá trị",
...@@ -1399,6 +1400,7 @@ export const translations = { ...@@ -1399,6 +1400,7 @@ export const translations = {
updateSuccess: "Đã cập nhật cấu hình thành công", updateSuccess: "Đã cập nhật cấu hình thành công",
toggleSuccess: "Đã thay đổi trạng thái cờ tính năng", toggleSuccess: "Đã thay đổi trạng thái cờ tính năng",
deleteSuccess: "Đã xóa cấu hình thành công", deleteSuccess: "Đã xóa cấu hình thành công",
syncEnvSuccess: "Đã đồng bộ toàn bộ giá trị cấu hình từ file .env thành công",
error: "Đã có lỗi xảy ra, vui lòng thử lại", error: "Đã có lỗi xảy ra, vui lòng thử lại",
}, },
}, },
...@@ -1447,7 +1449,7 @@ export const translations = { ...@@ -1447,7 +1449,7 @@ export const translations = {
jobLabel: "Tác vụ đang chọn:", jobLabel: "Tác vụ đang chọn:",
cronLabel: "Lịch biểu:", cronLabel: "Lịch biểu:",
paramsLabel: "Tham số tùy biến:", paramsLabel: "Tham số tùy biến:",
paramsPlaceholder: '{\n "retentionDays": 30\n}', paramsPlaceholder: '{\n "retentionDays": 7\n}',
paramsHint: "Để trống nếu muốn sử dụng tham số mặc định của hệ thống.", paramsHint: "Để trống nếu muốn sử dụng tham số mặc định của hệ thống.",
executing: "Đang thực thi tác vụ...", executing: "Đang thực thi tác vụ...",
runBtn: "Bắt Đầu Chạy", runBtn: "Bắt Đầu Chạy",
...@@ -2813,6 +2815,7 @@ export const translations = { ...@@ -2813,6 +2815,7 @@ export const translations = {
editBtn: "Edit", editBtn: "Edit",
deleteBtn: "Delete configuration", deleteBtn: "Delete configuration",
refreshBtn: "Refresh", refreshBtn: "Refresh",
syncEnvBtn: "Sync from .env",
table: { table: {
key: "Configuration Key", key: "Configuration Key",
value: "Current Value", value: "Current Value",
...@@ -2864,6 +2867,7 @@ export const translations = { ...@@ -2864,6 +2867,7 @@ export const translations = {
updateSuccess: "Configuration updated successfully", updateSuccess: "Configuration updated successfully",
toggleSuccess: "Feature flag state toggled successfully", toggleSuccess: "Feature flag state toggled successfully",
deleteSuccess: "Configuration deleted successfully", deleteSuccess: "Configuration deleted successfully",
syncEnvSuccess: "Configurations synchronized from .env successfully",
error: "An error occurred, please try again", error: "An error occurred, please try again",
}, },
}, },
...@@ -2912,7 +2916,7 @@ export const translations = { ...@@ -2912,7 +2916,7 @@ export const translations = {
jobLabel: "Selected Task:", jobLabel: "Selected Task:",
cronLabel: "Cron Schedule:", cronLabel: "Cron Schedule:",
paramsLabel: "Custom Parameters:", paramsLabel: "Custom Parameters:",
paramsPlaceholder: '{\n "retentionDays": 30\n}', paramsPlaceholder: '{\n "retentionDays": 7\n}',
paramsHint: "Leave blank to use the system default parameters.", paramsHint: "Leave blank to use the system default parameters.",
executing: "Executing task...", executing: "Executing task...",
runBtn: "Execute Now", runBtn: "Execute Now",
......
...@@ -96,4 +96,17 @@ export const systemConfigService = { ...@@ -96,4 +96,17 @@ export const systemConfigService = {
async deleteConfig(key: string): Promise<void> { async deleteConfig(key: string): Promise<void> {
await apiClient.delete(`/system/configs/${encodeURIComponent(key)}`); await apiClient.delete(`/system/configs/${encodeURIComponent(key)}`);
}, },
/**
* Đồng bộ toàn bộ cấu hình từ biến môi trường (.env) vào Database
* POST /api/proxy/system/configs/sync-env
*/
async syncFromEnv(): Promise<{ syncedCount: number; keys: string[] }> {
const response = await apiClient.post<{
success: boolean;
data: { syncedCount: number; keys: string[] };
message?: string;
}>("/system/configs/sync-env");
return response.data.data;
},
}; };
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment