Files
Jyotisha/frontend/src/components/admin/feature-flags-management.tsx
T
Jesse_Chen c712787ec2
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
fix(admin): remove manual reasons and repair code creation
2026-08-16 16:49:11 +08:00

165 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { PlusOutlined } from "@ant-design/icons";
import { useGetIdentity } from "@refinedev/core";
import { useTable } from "@refinedev/antd";
import { List } from "@refinedev/antd";
import { App, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Table, Typography, type TableColumnsType } from "antd";
import { useState } from "react";
import { adminRequestJson, type AdminIdentity } from "@/lib/admin/providers";
import { ConfirmActionModal } from "./confirm-action-modal";
import { formatAdminDate } from "./resource-table";
const { Text } = Typography;
type FeatureFlag = {
id: string;
flagKey: string;
version: number;
enabled: boolean;
rolloutPercentage: number;
config: Record<string, unknown>;
status: string;
createdAt: string;
publishedAt: string | null;
};
type FlagFilters = { q?: string; status?: string };
type FlagForm = {
flagKey: string;
enabled: boolean;
rolloutPercentage: number;
configJson: string;
};
export default function FeatureFlagsManagement() {
const { message } = App.useApp();
const { data: identity } = useGetIdentity<AdminIdentity>();
const table = useTable<FeatureFlag, { message: string; statusCode: number }, FlagFilters>({
resource: "feature-flags",
syncWithLocation: true,
pagination: { pageSize: 20 },
onSearch: ({ q, status }) => [
{ field: "q", operator: "contains", value: q },
{ field: "status", operator: "eq", value: status },
],
});
const [form] = Form.useForm<FlagForm>();
const [editing, setEditing] = useState<FeatureFlag | null>(null);
const [open, setOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [publishingId, setPublishingId] = useState<string | null>(null);
const [publishTarget, setPublishTarget] = useState<FeatureFlag | null>(null);
const canWrite = Boolean(identity?.permissions.includes("ops.flags.write"));
function edit(item?: FeatureFlag) {
setEditing(item ?? null);
form.setFieldsValue(item ? {
flagKey: item.flagKey,
enabled: item.enabled,
rolloutPercentage: item.rolloutPercentage,
configJson: JSON.stringify(item.config, null, 2),
} : {
flagKey: "",
enabled: false,
rolloutPercentage: 0,
configJson: "{}",
});
setOpen(true);
}
async function save(values: FlagForm) {
setSaving(true);
try {
let config: unknown;
try {
config = JSON.parse(values.configJson);
} catch {
throw new Error("配置 JSON 格式不正确");
}
await adminRequestJson("/api/admin/feature-flags", {
method: "POST",
body: JSON.stringify({
action: "save",
id: editing?.id ?? null,
flagKey: values.flagKey.trim(),
enabled: values.enabled,
rolloutPercentage: values.rolloutPercentage,
config,
expectedVersion: editing?.version ?? null,
}),
});
message.success("功能开关草稿已保存");
setOpen(false);
await table.tableQuery.refetch();
} catch (error) {
message.error(error instanceof Error ? error.message : "保存失败");
} finally {
setSaving(false);
}
}
async function publish(item: FeatureFlag) {
setPublishingId(item.id);
try {
await adminRequestJson("/api/admin/feature-flags", {
method: "POST",
body: JSON.stringify({ action: "publish", id: item.id, expectedVersion: item.version }),
});
message.success("功能开关已发布");
await table.tableQuery.refetch();
setPublishTarget(null);
} finally {
setPublishingId(null);
}
}
const columns: TableColumnsType<FeatureFlag> = [
{ title: "开关", render: (_, item) => <Space direction="vertical" size={0}><Text strong>{item.flagKey}</Text><Text type="secondary">v{item.version}</Text></Space> },
{ title: "状态", render: (_, item) => <Text>{item.status === "published" ? "已发布" : item.status === "draft" ? "草稿" : item.status} · {item.enabled ? "已开启" : "已关闭"}</Text> },
{ title: "灰度", dataIndex: "rolloutPercentage", render: (value) => `${value}%` },
{ title: "配置", dataIndex: "config", render: (value) => <Text code>{JSON.stringify(value)}</Text> },
{ title: "发布时间", dataIndex: "publishedAt", render: formatAdminDate },
{
title: "操作",
fixed: "right",
render: (_, item) => <Space>{canWrite && <Button type="link" onClick={() => edit(item)}>编辑草稿</Button>}{canWrite && item.status !== "published" && <Button type="link" loading={publishingId === item.id} onClick={() => setPublishTarget(item)}>发布</Button>}</Space>,
},
];
return <List title="功能开关" headerButtons={canWrite ? <Button type="primary" icon={<PlusOutlined />} onClick={() => edit()}>新增开关</Button> : null}>
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
<Form {...table.searchFormProps} layout="inline" style={{ rowGap: 8 }}>
<Form.Item name="q" label="搜索"><Input.Search allowClear placeholder="开关键" /></Form.Item>
<Form.Item name="status" label="状态">
<Select
allowClear
placeholder="全部状态"
style={{ minWidth: 140 }}
options={["draft", "published", "retired"].map((value) => ({ value, label: value }))}
/>
</Form.Item>
</Form>
<Table {...table.tableProps} columns={columns} rowKey="id" scroll={{ x: "max-content" }} />
</Space>
<Modal title={editing ? `编辑 ${editing.flagKey}` : "新增功能开关"} open={open} okText="保存草稿" cancelText="取消" confirmLoading={saving} onOk={() => form.submit()} onCancel={() => setOpen(false)} destroyOnHidden>
<Form<FlagForm> form={form} layout="vertical" onFinish={save}>
<Form.Item name="flagKey" label="开关键" rules={[{ required: true }, { pattern: /^[a-z][a-z0-9._-]{1,99}$/ }]}><Input disabled={Boolean(editing)} /></Form.Item>
<Form.Item name="rolloutPercentage" label="灰度百分比" rules={[{ required: true }]}><InputNumber min={0} max={100} precision={0} style={{ width: "100%" }} /></Form.Item>
<Form.Item name="enabled" label="启用" valuePropName="checked"><Switch /></Form.Item>
<Form.Item name="configJson" label="配置 JSON" rules={[{ required: true }]}><Input.TextArea rows={6} spellCheck={false} /></Form.Item>
</Form>
</Modal>
<ConfirmActionModal
open={Boolean(publishTarget)}
title={`发布功能开关${publishTarget ? `${publishTarget.flagKey}` : ""}`}
okText="确认发布"
confirmLoading={Boolean(publishingId)}
onCancel={() => setPublishTarget(null)}
onConfirm={() => publish(publishTarget!)}
/>
</List>;
}