Files
Jyotisha/frontend/src/components/admin/feature-flags-management.tsx
T
Jesse_Chen 945d61fa1c
Independent Staging Quality Gate / validate (push) Failing after 12m38s
Independent Staging Quality Gate / publish (push) Has been skipped
fix(admin): remove redundant confirmations and repair code access
2026-08-16 17:55:15 +08:00

156 lines
6.3 KiB
TypeScript

"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 { 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 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();
} catch (error) {
message.error(error instanceof Error ? error.message : "发布失败");
} 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={() => void publish(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>
</List>;
}