424 lines
15 KiB
TypeScript
424 lines
15 KiB
TypeScript
import React, { useState } from "react";
|
||
import { agentApiService } from "@/modules/agent/api/agent.api.service";
|
||
import { FiKey, FiPlus, FiTrash2, FiCopy, FiCheck, FiX } from "react-icons/fi";
|
||
|
||
interface RegistrationTokenForm {
|
||
label: string;
|
||
}
|
||
|
||
interface RegistrationResult {
|
||
label: string;
|
||
token: string;
|
||
}
|
||
|
||
export const RegistrationTokenPage: React.FC = () => {
|
||
const [tokens, setTokens] = useState<RegistrationTokenForm[]>([
|
||
{ label: "" },
|
||
]);
|
||
const [results, setResults] = useState<RegistrationResult[]>([]);
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
||
|
||
const handleTokenChange = (index: number, label: string) => {
|
||
const newTokens = [...tokens];
|
||
newTokens[index] = { label };
|
||
setTokens(newTokens);
|
||
};
|
||
|
||
const handleAddToken = () => {
|
||
setTokens([...tokens, { label: "" }]);
|
||
};
|
||
|
||
const handleRemoveToken = (index: number) => {
|
||
const newTokens = tokens.filter((_, i) => i !== index);
|
||
setTokens(newTokens);
|
||
};
|
||
|
||
const handleCopyToken = async (token: string, index: number) => {
|
||
await navigator.clipboard.writeText(token);
|
||
setCopiedIndex(index);
|
||
setTimeout(() => setCopiedIndex(null), 2000);
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
|
||
// Валидация
|
||
const validTokens = tokens.filter((t) => t.label.trim());
|
||
if (validTokens.length === 0) {
|
||
setError("Введите хотя бы одну метку для токена");
|
||
return;
|
||
}
|
||
|
||
setIsSubmitting(true);
|
||
setError(null);
|
||
setSuccessMessage(null);
|
||
setResults([]);
|
||
|
||
try {
|
||
const createdTokens: RegistrationResult[] = [];
|
||
|
||
for (const tokenData of validTokens) {
|
||
const response = await agentApiService.createRegistrationToken({
|
||
label: tokenData.label,
|
||
});
|
||
|
||
// API возвращает объект с токеном
|
||
const token = response.token || Object.values(response)[0] as string;
|
||
|
||
createdTokens.push({
|
||
label: tokenData.label,
|
||
token,
|
||
});
|
||
}
|
||
|
||
setResults(createdTokens);
|
||
setSuccessMessage(
|
||
`Успешно создано ${createdTokens.length} токен(ов)`
|
||
);
|
||
setTokens([{ label: "" }]);
|
||
} catch (err) {
|
||
setError(
|
||
err instanceof Error
|
||
? err.message
|
||
: "Ошибка при создании токенов"
|
||
);
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const inputStyle: React.CSSProperties = {
|
||
width: "100%",
|
||
padding: "10px 12px",
|
||
border: "1px solid var(--border)",
|
||
borderRadius: "8px",
|
||
backgroundColor: "var(--input-bg)",
|
||
color: "var(--text-primary)",
|
||
fontSize: "14px",
|
||
transition: "border-color 0.2s, box-shadow 0.2s",
|
||
};
|
||
|
||
const labelStyle: React.CSSProperties = {
|
||
display: "block",
|
||
marginBottom: "8px",
|
||
color: "var(--text-secondary)",
|
||
fontSize: "14px",
|
||
fontWeight: 500,
|
||
};
|
||
|
||
return (
|
||
<div
|
||
className="min-h-screen py-8 px-4"
|
||
style={{ backgroundColor: "var(--bg-primary)" }}
|
||
>
|
||
<div style={{ maxWidth: "900px", margin: "0 auto" }}>
|
||
{/* Header */}
|
||
<div className="mb-8">
|
||
<div className="flex items-center gap-4 mb-4">
|
||
<div
|
||
className="w-14 h-14 rounded-xl flex items-center justify-center"
|
||
style={{ backgroundColor: "var(--bg-secondary)" }}
|
||
>
|
||
<FiKey className="w-7 h-7" style={{ color: "var(--accent)" }} />
|
||
</div>
|
||
<div>
|
||
<h1
|
||
className="text-3xl font-bold mb-1"
|
||
style={{ color: "var(--text-primary)" }}
|
||
>
|
||
Регистрация токенов для агентов
|
||
</h1>
|
||
<p style={{ color: "var(--text-secondary)", fontSize: "16px" }}>
|
||
Создайте токены для регистрации новых агентов
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit}>
|
||
{/* Token Forms */}
|
||
<div className="space-y-5">
|
||
{tokens.map((token, index) => (
|
||
<div
|
||
key={index}
|
||
className="rounded-2xl shadow-lg border"
|
||
style={{
|
||
backgroundColor: "var(--card-bg)",
|
||
borderColor: "var(--border)",
|
||
padding: "24px",
|
||
marginBottom: "20px",
|
||
}}
|
||
>
|
||
{/* Header */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
marginBottom: "20px",
|
||
paddingBottom: "16px",
|
||
borderBottom: "1px solid var(--border)",
|
||
}}
|
||
>
|
||
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
||
<div
|
||
className="w-10 h-10 rounded-lg flex items-center justify-center"
|
||
style={{ backgroundColor: "var(--bg-secondary)" }}
|
||
>
|
||
<FiKey style={{ color: "var(--accent)", fontSize: "20px" }} />
|
||
</div>
|
||
<h3
|
||
style={{
|
||
margin: 0,
|
||
color: "var(--text-primary)",
|
||
fontSize: "18px",
|
||
fontWeight: 600,
|
||
}}
|
||
>
|
||
Токен #{index + 1}
|
||
</h3>
|
||
</div>
|
||
{tokens.length > 1 && (
|
||
<button
|
||
type="button"
|
||
onClick={() => handleRemoveToken(index)}
|
||
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-all"
|
||
style={{
|
||
background: "var(--error-bg)",
|
||
color: "var(--error-text)",
|
||
border: "1px solid var(--error-border)",
|
||
cursor: "pointer",
|
||
fontSize: "14px",
|
||
fontWeight: 500,
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
e.currentTarget.style.background = "var(--error-text)";
|
||
e.currentTarget.style.color = "#fff";
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.background = "var(--error-bg)";
|
||
e.currentTarget.style.color = "var(--error-text)";
|
||
}}
|
||
>
|
||
<FiTrash2 size={14} />
|
||
Удалить
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Label Input */}
|
||
<div>
|
||
<label style={labelStyle}>
|
||
<span
|
||
style={{ display: "flex", alignItems: "center", gap: "6px" }}
|
||
>
|
||
<FiKey size={14} />
|
||
Метка токена *
|
||
</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={token.label}
|
||
onChange={(e) => handleTokenChange(index, e.target.value)}
|
||
required
|
||
style={inputStyle}
|
||
onFocus={(e) => {
|
||
e.currentTarget.style.borderColor = "var(--border-focus)";
|
||
e.currentTarget.style.boxShadow = `0 0 0 3px var(--border-focus)30`;
|
||
}}
|
||
onBlur={(e) => {
|
||
e.currentTarget.style.borderColor = "var(--border)";
|
||
e.currentTarget.style.boxShadow = "none";
|
||
}}
|
||
placeholder="agent-production-1"
|
||
/>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Add Token Button */}
|
||
<button
|
||
type="button"
|
||
onClick={handleAddToken}
|
||
className="w-full flex items-center justify-center gap-2 py-3.5 px-4 rounded-xl border-2 border-dashed transition-all mb-6 font-medium"
|
||
style={{
|
||
borderColor: "var(--border)",
|
||
backgroundColor: "transparent",
|
||
color: "var(--accent)",
|
||
fontSize: "15px",
|
||
cursor: "pointer",
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
e.currentTarget.style.borderColor = "var(--accent)";
|
||
e.currentTarget.style.backgroundColor = "var(--accent)10";
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.borderColor = "var(--border)";
|
||
e.currentTarget.style.backgroundColor = "transparent";
|
||
}}
|
||
>
|
||
<FiPlus size={18} />
|
||
Добавить ещё один токен
|
||
</button>
|
||
|
||
{/* Messages */}
|
||
{successMessage && (
|
||
<div
|
||
className="mb-6 p-4 rounded-lg border text-sm"
|
||
style={{
|
||
backgroundColor: "var(--success-bg)",
|
||
borderColor: "var(--success-border)",
|
||
color: "var(--success-text)",
|
||
}}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<span>{successMessage}</span>
|
||
<button
|
||
onClick={() => setSuccessMessage(null)}
|
||
style={{ background: "none", border: "none", cursor: "pointer", color: "inherit" }}
|
||
>
|
||
<FiX size={16} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{error && (
|
||
<div
|
||
className="mb-6 p-4 rounded-lg border text-sm"
|
||
style={{
|
||
backgroundColor: "var(--error-bg)",
|
||
borderColor: "var(--error-border)",
|
||
color: "var(--error-text)",
|
||
}}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<span>{error}</span>
|
||
<button
|
||
onClick={() => setError(null)}
|
||
style={{ background: "none", border: "none", cursor: "pointer", color: "inherit" }}
|
||
>
|
||
<FiX size={16} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Submit Button */}
|
||
<button
|
||
type="submit"
|
||
disabled={isSubmitting}
|
||
className="w-full flex items-center justify-center gap-2 px-4 py-3.5 rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed font-medium text-base mb-8"
|
||
style={{
|
||
backgroundColor: isSubmitting
|
||
? "var(--bg-secondary)"
|
||
: "var(--button-primary)",
|
||
color: "var(--button-primary-text)",
|
||
boxShadow: isSubmitting
|
||
? "none"
|
||
: "0 4px 14px var(--shadow-color)",
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
if (!isSubmitting) {
|
||
e.currentTarget.style.backgroundColor =
|
||
"var(--button-primary-hover)";
|
||
}
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.backgroundColor = isSubmitting
|
||
? "var(--bg-secondary)"
|
||
: "var(--button-primary)";
|
||
}}
|
||
>
|
||
{isSubmitting ? (
|
||
<>
|
||
<div className="w-5 h-5 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||
Создание токенов...
|
||
</>
|
||
) : (
|
||
<>
|
||
<FiKey size={18} />
|
||
Создать {tokens.filter((t) => t.label.trim()).length || 1} токен(ов)
|
||
</>
|
||
)}
|
||
</button>
|
||
|
||
{/* Results */}
|
||
{results.length > 0 && (
|
||
<div>
|
||
<h2
|
||
className="text-xl font-bold mb-4"
|
||
style={{ color: "var(--text-primary)" }}
|
||
>
|
||
Созданные токены
|
||
</h2>
|
||
<div className="space-y-4">
|
||
{results.map((result, index) => (
|
||
<div
|
||
key={index}
|
||
className="rounded-xl border p-4"
|
||
style={{
|
||
backgroundColor: "var(--card-bg)",
|
||
borderColor: "var(--border)",
|
||
}}
|
||
>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<span
|
||
className="font-medium"
|
||
style={{ color: "var(--text-primary)" }}
|
||
>
|
||
{result.label}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleCopyToken(result.token, index)}
|
||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg transition-all text-xs font-medium"
|
||
style={{
|
||
backgroundColor:
|
||
copiedIndex === index
|
||
? "var(--success-text)"
|
||
: "var(--accent)",
|
||
color:
|
||
copiedIndex === index
|
||
? "#fff"
|
||
: "var(--accent-text)",
|
||
}}
|
||
>
|
||
{copiedIndex === index ? (
|
||
<>
|
||
<FiCheck size={12} />
|
||
Скопировано
|
||
</>
|
||
) : (
|
||
<>
|
||
<FiCopy size={12} />
|
||
Копировать
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
<code
|
||
className="block p-3 rounded-lg text-xs font-mono break-all"
|
||
style={{
|
||
backgroundColor: "var(--bg-secondary)",
|
||
color: "var(--accent)",
|
||
border: "1px solid var(--border)",
|
||
}}
|
||
>
|
||
{result.token}
|
||
</code>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|