From 9ea671b57c34bebc4cefc7829fd846b2420b0109 Mon Sep 17 00:00:00 2001 From: Redsandyg Date: Mon, 9 Jun 2025 12:53:07 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=20AccountIntegration=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=D0=BC=D0=B8=20=D0=B8=20=D1=82=D0=BE=D0=BA?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=D0=BC=D0=B8.=20=D0=9E=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=81=D1=82=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=86=D0=B0=20=D0=B0=D0=BA=D0=BA=D0=B0=D1=83=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F,=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=B0=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B8=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=BE=D0=B2.=20?= =?UTF-8?q?=D0=A2=D0=B0=D0=BA=D0=B6=D0=B5=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D1=8B=20CreateTokenDialog=20=D0=B8=20Integra?= =?UTF-8?q?tionTokensTable=20=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D1=8F=20=D1=81?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BC=20=D0=B8=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82=D0=BE=D0=BA=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/account/page.tsx | 8 +- src/components/AccountIntegration.tsx | 121 +++++++++++++++++ src/components/CreateTokenDialog.tsx | 157 ++++++++++++++++++++++ src/components/IntegrationTokensTable.tsx | 129 ++++++++++++++++++ 4 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 src/components/AccountIntegration.tsx create mode 100644 src/components/CreateTokenDialog.tsx create mode 100644 src/components/IntegrationTokensTable.tsx diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx index 8e93a69..72626eb 100644 --- a/src/app/account/page.tsx +++ b/src/app/account/page.tsx @@ -17,13 +17,15 @@ import { Lock as KeyIcon, Visibility as EyeIcon, VisibilityOff as EyeOffIcon, - Notifications as BellIcon + Notifications as BellIcon, + IntegrationInstructions as IntegrationIcon } from "@mui/icons-material"; import AccountProfile from "../../components/AccountProfile"; import AccountSecurity from "../../components/AccountSecurity"; import AccountNotifications from "../../components/AccountNotifications"; import AccountAgentTransactionSection from "../../components/AccountAgentTransactionSection"; import TabsNav from "../../components/TabsNav"; +import AccountIntegration from "../../components/AccountIntegration"; const initialNotifications = { emailNotifications: true, @@ -48,6 +50,7 @@ export default function AccountPage() { { id: "security", label: "Безопасность", icon: }, { id: "notifications", label: "Уведомления", icon: }, { id: "agent-transactions", label: "Транзакции агентов", icon: }, + { id: "integration", label: "Интеграции", icon: }, ]; return ( @@ -69,6 +72,9 @@ export default function AccountPage() { {activeTab === "agent-transactions" && ( )} + {activeTab === "integration" && ( + + )} ); diff --git a/src/components/AccountIntegration.tsx b/src/components/AccountIntegration.tsx new file mode 100644 index 0000000..d1f1d04 --- /dev/null +++ b/src/components/AccountIntegration.tsx @@ -0,0 +1,121 @@ +"use client"; +import React, { useState, useMemo } from "react"; +import { + Box, + Button, + Typography, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + IconButton +} from "@mui/material"; +import CreateTokenDialog from "./CreateTokenDialog"; +import IntegrationTokensTable from "./IntegrationTokensTable"; +import styles from "../styles/account.module.css"; + +// Компонент для управления интеграциями и токенами +interface Token { + id: string; + description: string; + token: string; + createdAt: string; + lastUsedAt?: string; +} + +const AccountIntegration = () => { + const [tokens, setTokens] = useState([]); + const [openCreateDialog, setOpenCreateDialog] = useState(false); + const [tokenDescription, setTokenDescription] = useState(""); + const [showTokenWarning, setShowTokenWarning] = useState(false); + const [showTokenCreatedSuccess, setShowTokenCreatedSuccess] = useState(false); + const [openEditDialog, setOpenEditDialog] = useState(false); + const [editingToken, setEditingToken] = useState(null); + + const handleOpenCreateDialog = () => { + setOpenCreateDialog(true); + setTokenDescription(""); + setShowTokenWarning(false); + }; + + const handleCloseCreateDialog = () => { + setOpenCreateDialog(false); + }; + + const handleOpenEditDialog = (token: Token) => { + setEditingToken(token); + setOpenEditDialog(true); + }; + + const handleCloseEditDialog = () => { + setOpenEditDialog(false); + setEditingToken(null); + }; + + const handleTokenGenerate = (description: string, newTokenValue: string) => { + const newToken: Token = { + id: (tokens.length + 1).toString(), + description: description, + token: newTokenValue, + createdAt: new Date().toLocaleString(), + lastUsedAt: "Никогда", + }; + setTokens([...tokens, newToken]); + setShowTokenCreatedSuccess(true); + setTimeout(() => setShowTokenCreatedSuccess(false), 1500); + }; + + const handleTokenUpdate = (updatedDescription: string) => { + if (editingToken) { + setTokens(prevTokens => + prevTokens.map(token => + token.id === editingToken.id ? { ...token, description: updatedDescription } : token + ) + ); + setOpenEditDialog(false); + setEditingToken(null); + } + }; + + const handleDeleteToken = (tokenId: string) => { + setTokens(prevTokens => prevTokens.filter(token => token.id !== tokenId)); + }; + + return ( + + + Управление токенами интеграции + + + + + + + + + {showTokenCreatedSuccess && ( +
+ Токен успешно создан! +
+ )} +
+ ); +}; + +export default AccountIntegration; \ No newline at end of file diff --git a/src/components/CreateTokenDialog.tsx b/src/components/CreateTokenDialog.tsx new file mode 100644 index 0000000..925d39e --- /dev/null +++ b/src/components/CreateTokenDialog.tsx @@ -0,0 +1,157 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import { + Box, + Button, + TextField, + Typography, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Snackbar, + Alert, +} from "@mui/material"; +import { ContentCopy as ContentCopyIcon } from "@mui/icons-material"; +import styles from "../styles/account.module.css"; + +interface CreateTokenDialogProps { + open: boolean; + onClose: () => void; + onTokenGenerate?: (description: string, token: string) => void; + isEditMode?: boolean; + initialDescription?: string; + onTokenUpdate?: (updatedDescription: string) => void; +} + +const CreateTokenDialog: React.FC = ({ + open, + onClose, + onTokenGenerate, + isEditMode, + initialDescription, + onTokenUpdate, +}) => { + const [tokenDescription, setTokenDescription] = useState(""); + const [generatedToken, setGeneratedToken] = useState(""); + const [showTokenWarning, setShowTokenWarning] = useState(false); + const [showCopySuccess, setShowCopySuccess] = useState(false); + + useEffect(() => { + if (!open) { + setTokenDescription(""); + setGeneratedToken(""); + setShowTokenWarning(false); + setShowCopySuccess(false); + } else if (isEditMode && initialDescription) { + setTokenDescription(initialDescription); + } else { + setTokenDescription(""); + setGeneratedToken(""); + setShowTokenWarning(false); + setShowCopySuccess(false); + } + }, [open, isEditMode, initialDescription]); + + const handleGenerateToken = () => { + if (tokenDescription.trim() === "") return; + + const newTokenValue = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + setGeneratedToken(newTokenValue); + setShowTokenWarning(true); + if (onTokenGenerate) { + onTokenGenerate(tokenDescription, newTokenValue); + } + }; + + const handleUpdateDescription = () => { + if (tokenDescription.trim() === "") return; + if (onTokenUpdate) { + onTokenUpdate(tokenDescription); + } + onClose(); + }; + + const handleCopy = () => { + if (!generatedToken) return; + if (typeof navigator !== "undefined" && navigator.clipboard) { + navigator.clipboard.writeText(generatedToken); + } else { + const textarea = document.createElement("textarea"); + textarea.value = generatedToken; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); + } + setShowCopySuccess(true); + setTimeout(() => setShowCopySuccess(false), 1500); + }; + + return ( + + {isEditMode ? "Редактировать описание токена" : "Создать новый токен"} + + {isEditMode || !generatedToken ? ( + <> + + {isEditMode ? "Введите новое описание для токена." : "Пожалуйста, введите описание для вашего нового токена."} + + setTokenDescription(e.target.value)} + /> + + ) : ( + + Ваш новый токен: + + + {generatedToken} + + + + {showTokenWarning && ( + + Внимание: Этот токен не может быть восстановлен. Пожалуйста, скопируйте его сейчас! + + )} + {showCopySuccess && ( +
+ Токен скопирован! +
+ )} +
+ )} +
+ + {!(isEditMode || generatedToken) ? ( + + ) : null} + + +
+ ); +}; + +export default CreateTokenDialog; \ No newline at end of file diff --git a/src/components/IntegrationTokensTable.tsx b/src/components/IntegrationTokensTable.tsx new file mode 100644 index 0000000..666ca81 --- /dev/null +++ b/src/components/IntegrationTokensTable.tsx @@ -0,0 +1,129 @@ +"use client"; +import React, { useMemo } from "react"; +import { + Box, + Button, + Typography, + IconButton +} from "@mui/material"; +import { MaterialReactTable, type MRT_ColumnDef, useMaterialReactTable } from "material-react-table"; +import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from "@mui/icons-material"; + +interface Token { + id: string; + description: string; + token: string; + createdAt: string; + lastUsedAt?: string; +} + +interface IntegrationTokensTableProps { + tokens: Token[]; + onOpenCreateDialog: () => void; + onOpenEditDialog: (token: Token) => void; + onDeleteToken: (tokenId: string) => void; +} + +const IntegrationTokensTable: React.FC = ({ + tokens, + onOpenCreateDialog, + onOpenEditDialog, + onDeleteToken, +}) => { + const columns = useMemo[]>( + () => [ + { + accessorKey: "description", + header: "Описание", + size: 200, + Cell: ({ renderedCellValue }) => renderedCellValue, + }, + { + accessorKey: "token", + header: "Токен", + size: 250, + Cell: ({ renderedCellValue }) => { + const tokenValue = renderedCellValue as string; + const maskedToken = tokenValue.substring(0, 5) + "***********************" + tokenValue.substring(tokenValue.length - 4); + return ( + + {maskedToken} + + ); + }, + }, + { + accessorKey: "createdAt", + header: "Дата создания", + size: 150, + Cell: ({ renderedCellValue }) => renderedCellValue, + }, + { + accessorKey: "lastUsedAt", + header: "Последнее использование", + size: 150, + Cell: ({ renderedCellValue }) => renderedCellValue, + }, + { + id: 'actions', + header: 'Действия', + size: 100, + Cell: ({ row }) => ( + + onOpenEditDialog(row.original)} + color="primary" + > + + + onDeleteToken(row.original.id)} + color="error" + > + + + + ), + }, + ], + [onOpenEditDialog, onDeleteToken], + ); + + const table = useMaterialReactTable({ + columns, + data: tokens, + enableColumnActions: false, + enableColumnFilters: true, + enablePagination: true, + enableSorting: true, + enableBottomToolbar: true, + enableTopToolbar: true, + enableDensityToggle: true, + enableGlobalFilter: true, + enableHiding: true, + renderEmptyRowsFallback: () => ( + + + У вас пока нет созданных токенов. + + + ), + muiTableBodyCellProps: { sx: { fontSize: 14 } }, + muiTableHeadCellProps: { sx: { fontWeight: 700 } }, + initialState: { pagination: { pageSize: 10, pageIndex: 0 } }, + renderTopToolbarCustomActions: () => ( + + ), + }); + + return ; +}; + +export default IntegrationTokensTable; \ No newline at end of file