Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants. Co-authored-by: Cursor <cursoragent@cursor.com>
180 lines
5.6 KiB
TypeScript
180 lines
5.6 KiB
TypeScript
"use client";
|
||
|
||
import { FormEvent, useCallback, useEffect, useState } from "react";
|
||
import { api, ApiError, type Feature } from "@/lib/api";
|
||
import {
|
||
AdminPageHeader,
|
||
Banner,
|
||
Button,
|
||
EmptyState,
|
||
Modal,
|
||
StatusBadge,
|
||
TextField,
|
||
TextareaField,
|
||
} from "@/components/admin/controls";
|
||
|
||
export default function AdminFeaturesPage() {
|
||
const [features, setFeatures] = useState<Feature[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState("");
|
||
const [open, setOpen] = useState(false);
|
||
|
||
const load = useCallback(async () => {
|
||
setLoading(true);
|
||
setError("");
|
||
try {
|
||
const page = await api.features.list(1, 100);
|
||
setFeatures(page.items);
|
||
} catch (e) {
|
||
setError(e instanceof ApiError ? e.message : "خطا در بارگذاری قابلیتها");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
void load();
|
||
}, [load]);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<AdminPageHeader
|
||
title="قابلیتها"
|
||
subtitle="قابلیتهای قابل اتصال به پلنها"
|
||
action={<Button onClick={() => setOpen(true)}>قابلیت جدید</Button>}
|
||
/>
|
||
|
||
{error && <Banner variant="error">{error}</Banner>}
|
||
|
||
<section className="overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-sm">
|
||
{loading ? (
|
||
<EmptyState message="در حال بارگذاری..." />
|
||
) : features.length === 0 ? (
|
||
<EmptyState message="هنوز قابلیتی تعریف نشده است." />
|
||
) : (
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full min-w-[640px] text-sm">
|
||
<thead className="bg-gray-50 text-right">
|
||
<tr>
|
||
<th className="px-4 py-3 font-semibold text-secondary">نام</th>
|
||
<th className="px-4 py-3 font-semibold text-secondary">کلید</th>
|
||
<th className="px-4 py-3 font-semibold text-secondary">سرویس</th>
|
||
<th className="px-4 py-3 font-semibold text-secondary">وضعیت</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{features.map((f) => (
|
||
<tr key={f.id} className="border-t border-gray-100">
|
||
<td className="px-4 py-3 font-medium text-secondary">{f.name}</td>
|
||
<td className="px-4 py-3 font-mono text-gray-600" dir="ltr">
|
||
{f.feature_key}
|
||
</td>
|
||
<td className="px-4 py-3 font-mono text-gray-600" dir="ltr">
|
||
{f.service_key}
|
||
</td>
|
||
<td className="px-4 py-3">
|
||
<StatusBadge status={f.is_active ? "active" : "inactive"} />
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
<CreateFeatureModal
|
||
open={open}
|
||
onClose={() => setOpen(false)}
|
||
onCreated={() => {
|
||
setOpen(false);
|
||
void load();
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function CreateFeatureModal({
|
||
open,
|
||
onClose,
|
||
onCreated,
|
||
}: {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
onCreated: () => void;
|
||
}) {
|
||
const [featureKey, setFeatureKey] = useState("");
|
||
const [name, setName] = useState("");
|
||
const [serviceKey, setServiceKey] = useState("");
|
||
const [description, setDescription] = useState("");
|
||
const [saving, setSaving] = useState(false);
|
||
const [error, setError] = useState("");
|
||
|
||
const handleSubmit = async (e: FormEvent) => {
|
||
e.preventDefault();
|
||
setError("");
|
||
setSaving(true);
|
||
try {
|
||
await api.features.create({
|
||
feature_key: featureKey.trim(),
|
||
name: name.trim(),
|
||
service_key: serviceKey.trim(),
|
||
description: description.trim() || null,
|
||
});
|
||
setFeatureKey("");
|
||
setName("");
|
||
setServiceKey("");
|
||
setDescription("");
|
||
onCreated();
|
||
} catch (err) {
|
||
setError(err instanceof ApiError ? err.message : "ایجاد قابلیت ناموفق بود");
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Modal open={open} title="ایجاد قابلیت جدید" onClose={onClose}>
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<TextField
|
||
label="کلید قابلیت"
|
||
required
|
||
dir="ltr"
|
||
className="font-mono"
|
||
value={featureKey}
|
||
onValue={setFeatureKey}
|
||
disabled={saving}
|
||
placeholder="sms.bulk_send"
|
||
/>
|
||
<TextField label="نام" required value={name} onValue={setName} disabled={saving} />
|
||
<TextField
|
||
label="کلید سرویس"
|
||
required
|
||
dir="ltr"
|
||
className="font-mono"
|
||
value={serviceKey}
|
||
onValue={setServiceKey}
|
||
disabled={saving}
|
||
placeholder="sms-service"
|
||
/>
|
||
<TextareaField
|
||
label="توضیحات"
|
||
value={description}
|
||
onValue={setDescription}
|
||
disabled={saving}
|
||
/>
|
||
{error && <Banner variant="error">{error}</Banner>}
|
||
<div className="flex justify-end gap-3 pt-2">
|
||
<Button variant="outline" onClick={onClose} type="button">
|
||
انصراف
|
||
</Button>
|
||
<Button type="submit" loading={saving} loadingText="در حال ایجاد...">
|
||
ایجاد قابلیت
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</Modal>
|
||
);
|
||
}
|