{/* Сайдбар */}
diff --git a/src/Components/TreeChart/FlowChart.jsx b/src/Components/TreeChart/FlowChart.jsx
index 63e3c85..082cdd6 100644
--- a/src/Components/TreeChart/FlowChart.jsx
+++ b/src/Components/TreeChart/FlowChart.jsx
@@ -1,170 +1,70 @@
-import React, { useCallback, useEffect, useState, useMemo } from 'react';
-import ReactFlow, { Controls, Background, useNodesState, useEdgesState } from 'reactflow';
+import React, { useEffect, useMemo } from 'react';
+import ReactFlow, { Controls, Background } from 'reactflow';
import 'reactflow/dist/style.css';
-import { statusManager1, getStatusColor } from './dataUtils';
+import { debounce } from 'lodash';
+import { useFlowChart } from './FlowChartComponents/useFlowChart';
+import { useNodeHandlers } from './FlowChartComponents/useNodeHandlers';
+import { useDataParser } from './FlowChartComponents/DataParser';
+import NodeWrapper from './FlowChartComponents/NodeWrapper'; // Исправленный импорт
+
+// Определяем кастомные типы узлов
+const nodeTypes = {
+ customNode: NodeWrapper // Тип должен соответствовать тому, что вы указываете в parseData
+};
const FlowChart = ({ data }) => {
- const [nodes, setNodes, onNodesChange] = useNodesState([]);
- const [edges, setEdges, onEdgesChange] = useEdgesState([]);
- const [collapsedNodes, setCollapsedNodes] = useState(new Set()); // Состояние для свернутых узлов
- const [nodePositions, setNodePositions] = useState({}); // Состояние для сохранения позиций узлов
+ const {
+ nodes,
+ edges,
+ nodePositions,
+ setNodes,
+ setEdges,
+ onNodesChange,
+ onEdgesChange,
+ setNodePositions,
+ collapsedNodes,
+ toggleNodeCollapse
+ } = useFlowChart(data);
- // Обновление статусов
- useEffect(() => {
- const updateStatuses = (data) => {
- statusManager1.updateStatuses(data);
- };
+ const { parseData } = useDataParser(nodePositions, collapsedNodes);
- updateStatuses(data); // Обновляем статусы только при изменении данных
- }, [data]); // Зависимость от data
-
- // Функция для построения радиального графа
- const parseData = useCallback(
- (data) => {
- const nodes = [];
- const edges = [];
-
- const centerX = 500; // Центр графа по X
- const centerY = 400; // Центр графа по Y
- const levelRadius = 150; // Расстояние между уровнями
-
- // Основная рекурсивная функция
- const traverse = (item, parentId = null, level = 0, angleStart = 0, angleEnd = 2 * Math.PI) => {
- const nodeId = item.id;
-
- // Угол узла на круге
- const angle = (angleStart + angleEnd) / 2; // Угол для текущего узла
- const nodeX = centerX + Math.cos(angle) * level * levelRadius;
- const nodeY = centerY + Math.sin(angle) * level * levelRadius;
-
- // Используем сохраненную позицию, если она есть
- const savedPosition = nodePositions[nodeId];
- const position = savedPosition || { x: nodeX, y: nodeY };
-
- const node = {
- id: nodeId,
- position,
- style: {
- width: 50,
- height: 50,
- borderRadius: '50%', // Делаем круги
- backgroundColor: getStatusColor(item.status),
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- color: 'white',
- border: '2px solid #fff',
- },
- data: { label: item.title },
- };
-
- nodes.push(node);
-
- // Создаем ребро к родителю
- if (parentId) {
- edges.push({ id: `${parentId}-${nodeId}`, source: parentId, target: nodeId });
- }
-
- // Дочерние узлы (если узел не свернут)
- if (item.items && item.items.length > 0 && !collapsedNodes.has(nodeId)) {
- const angleStep = (angleEnd - angleStart) / item.items.length; // Шаг угла для дочерних узлов
- item.items.forEach((child, index) => {
- // Равномерно распределяем дочерние узлы вокруг родителя
- traverse(
- child,
- nodeId,
- level + 1,
- angleStart + index * angleStep, // Начальный угол для дочернего узла
- angleStart + (index + 1) * angleStep // Конечный угол для дочернего узла
- );
- });
- }
- };
-
- // Начинаем с центрального узла
- const centerNode = {
- id: data.id,
- position: { x: centerX, y: centerY },
- style: {
- width: 50,
- height: 50,
- borderRadius: '50%',
- backgroundColor: getStatusColor(data.status),
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- color: 'white',
- border: '2px solid #fff',
- },
- data: { label: data.title },
- };
-
- nodes.push(centerNode);
-
- // Обрабатываем дочерние узлы центрального узла
- if (data.items && data.items.length > 0 && !collapsedNodes.has(data.id)) {
- const angleStep = (2 * Math.PI) / data.items.length; // Равномерно распределяем вокруг центра
- data.items.forEach((child, index) => {
- traverse(
- child,
- data.id,
- 1, // Уровень 1 (первый круг вокруг центра)
- index * angleStep, // Начальный угол
- (index + 1) * angleStep // Конечный угол
- );
- });
- }
-
- return { nodes, edges };
- },
- [collapsedNodes, nodePositions] // Зависимость от collapsedNodes и nodePositions
+ const debouncedSetNodePositions = useMemo(
+ () => debounce(setNodePositions, 100),
+ [setNodePositions]
);
- // Обработчик клика по узлу
- const onNodeClick = useCallback(
- (event, node) => {
- const nodeId = node.id;
- const newCollapsedNodes = new Set(collapsedNodes);
+ const { onNodeDrag, onNodeDragStop } = useNodeHandlers(debouncedSetNodePositions);
- if (newCollapsedNodes.has(nodeId)) {
- newCollapsedNodes.delete(nodeId); // Разворачиваем узел
- } else {
- newCollapsedNodes.add(nodeId); // Сворачиваем узел
- }
-
- setCollapsedNodes(newCollapsedNodes); // Обновляем состояние
- },
- [collapsedNodes]
- );
-
- // Обработчик завершения перетаскивания узла
- const onNodeDragStop = useCallback(
- (event, node) => {
- // Сохраняем новую позицию узла
- setNodePositions((prevPositions) => ({
- ...prevPositions,
- [node.id]: node.position,
- }));
- },
- []
- );
-
- // Обновляем узлы и ребра при изменении данных
useEffect(() => {
const { nodes: initialNodes, edges: initialEdges } = parseData(data);
setNodes(initialNodes);
setEdges(initialEdges);
}, [data, parseData, setNodes, setEdges]);
+ const onNodeClick = (event, node) => {
+ if (node.data.hasChildren) {
+ toggleNodeCollapse(node.id);
+ }
+ };
+
+ useEffect(() => {
+ return () => {
+ debouncedSetNodePositions.cancel();
+ };
+ }, [debouncedSetNodePositions]);
+
return (
-
+
@@ -174,4 +74,4 @@ const FlowChart = ({ data }) => {
);
};
-export default React.memo(FlowChart); // Оптимизация рендера
\ No newline at end of file
+export default React.memo(FlowChart);
\ No newline at end of file
diff --git a/src/Components/TreeChart/FlowChartComponents/DataParser.jsx b/src/Components/TreeChart/FlowChartComponents/DataParser.jsx
new file mode 100644
index 0000000..73855cd
--- /dev/null
+++ b/src/Components/TreeChart/FlowChartComponents/DataParser.jsx
@@ -0,0 +1,104 @@
+import { useCallback } from 'react';
+import { isLeafNode } from './nodeUtils';
+import { getStatusColor } from '../dataUtils';
+
+export const useDataParser = (nodePositions, collapsedNodes) => {
+ const getNodeStyle = useCallback((item, isLeaf) => ({
+ width: isLeaf ? 40 : 50,
+ height: isLeaf ? 40 : 50,
+ borderRadius: '50%',
+ backgroundColor: getStatusColor(item.status),
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: 'black',
+ border: '2px solid #fff',
+ fontSize: isLeaf ? '0.8rem' : '1rem'
+ }), []);
+
+ const getCenterNodeStyle = useCallback((item) => ({
+ width: 60,
+ height: 60,
+ borderRadius: '50%',
+ backgroundColor: getStatusColor(item.status),
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: 'black',
+ border: '2px solid #fff',
+ fontSize: '1.2rem'
+ }), []);
+
+ const parseData = useCallback((data) => {
+ if (!data) return { nodes: [], edges: [] };
+
+ const nodes = [];
+ const edges = [];
+ const centerX = 500;
+ const centerY = 400;
+ const baseLevelRadius = 150;
+
+ const traverse = (item, parentId = null, level = 0, angleStart = 0, angleEnd = 2 * Math.PI, parentRadius = 0) => {
+ if (!item || collapsedNodes[parentId]) return; // Пропускаем свёрнутые узлы
+
+ const nodeId = item.id;
+ const items = item.items || [];
+ const isLeaf = isLeafNode(item);
+
+ const savedPosition = nodePositions[nodeId];
+ let position = savedPosition || {
+ x: Math.round(centerX + Math.cos((angleStart + angleEnd) / 2) * (parentRadius + baseLevelRadius)),
+ y: Math.round(centerY + Math.sin((angleStart + angleEnd) / 2) * (parentRadius + baseLevelRadius))
+ };
+
+ const node = {
+ id: nodeId,
+ position,
+ style: getNodeStyle(item, isLeaf),
+ data: { label: item.title, hasChildren: items.length > 0, collapsed: collapsedNodes[nodeId] }
+ };
+
+ nodes.push(node);
+
+ if (parentId) {
+ edges.push({
+ id: `${parentId}-${nodeId}`,
+ source: parentId,
+ target: nodeId,
+ style: { stroke: isLeaf ? '#aaa' : '#666', strokeWidth: isLeaf ? 1 : 2 }
+ });
+ }
+
+ if (!collapsedNodes[nodeId] && items.length > 0) {
+ const spreadAngle = angleEnd - angleStart;
+ items.forEach((child, index) => {
+ if (!child) return;
+ const itemAngleStart = angleStart + (index / items.length) * spreadAngle;
+ const itemAngleEnd = angleStart + ((index + 1) / items.length) * spreadAngle;
+ traverse(child, nodeId, level + 1, itemAngleStart, itemAngleEnd, parentRadius + baseLevelRadius);
+ });
+ }
+ };
+
+ const centerNode = {
+ id: data.id,
+ position: nodePositions[data.id] || { x: centerX, y: centerY },
+ style: getCenterNodeStyle(data),
+ data: { label: data.title, hasChildren: data.items.length > 0, collapsed: collapsedNodes[data.id] }
+ };
+
+ nodes.push(centerNode);
+
+ if (!collapsedNodes[data.id] && data.items.length > 0) {
+ const angleStep = (2 * Math.PI) / data.items.length;
+ data.items.forEach((child, index) => {
+ if (!child) return;
+ traverse(child, data.id, 1, index * angleStep, (index + 1) * angleStep, 0);
+ });
+ }
+
+ return { nodes, edges };
+ }, [nodePositions, collapsedNodes, getNodeStyle, getCenterNodeStyle]);
+
+ return { parseData };
+};
\ No newline at end of file
diff --git a/src/Components/TreeChart/FlowChartComponents/NodeWrapper.jsx b/src/Components/TreeChart/FlowChartComponents/NodeWrapper.jsx
new file mode 100644
index 0000000..af1283d
--- /dev/null
+++ b/src/Components/TreeChart/FlowChartComponents/NodeWrapper.jsx
@@ -0,0 +1,26 @@
+import React, { memo } from 'react';
+
+const NodeWrapper = memo(({ id, data, selected, style }) => {
+ return (
+
+ {data.label}
+ {data.hasChildren && (
+
+ {data.collapsed ? '+' : '-'}
+
+ )}
+
+ );
+});
+
+export default NodeWrapper;
diff --git a/src/Components/TreeChart/FlowChartComponents/nodeUtils.jsx b/src/Components/TreeChart/FlowChartComponents/nodeUtils.jsx
new file mode 100644
index 0000000..5aab37a
--- /dev/null
+++ b/src/Components/TreeChart/FlowChartComponents/nodeUtils.jsx
@@ -0,0 +1,3 @@
+export const isLeafNode = (item) => {
+ return !item.items || item.items.length === 0;
+};
\ No newline at end of file
diff --git a/src/Components/TreeChart/FlowChartComponents/useFlowChart.jsx b/src/Components/TreeChart/FlowChartComponents/useFlowChart.jsx
new file mode 100644
index 0000000..b435fb5
--- /dev/null
+++ b/src/Components/TreeChart/FlowChartComponents/useFlowChart.jsx
@@ -0,0 +1,46 @@
+import { useState, useCallback, useEffect } from 'react';
+import { useNodesState, useEdgesState } from 'reactflow';
+import { statusManager1 } from '../dataUtils';
+
+export const useFlowChart = (initialData) => {
+ const [nodes, setNodes, onNodesChange] = useNodesState([]);
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
+ const [nodePositions, setNodePositions] = useState({});
+ const [collapsedNodes, setCollapsedNodes] = useState({}); // Добавили
+
+ const toggleNodeCollapse = useCallback((nodeId) => {
+ setCollapsedNodes((prev) => ({
+ ...prev,
+ [nodeId]: !prev[nodeId]
+ }));
+ }, []);
+
+ const initializeNodePositions = useCallback((nodes) => {
+ const positions = {};
+ nodes.forEach(node => {
+ positions[node.id] = node.position;
+ });
+ setNodePositions(positions);
+ }, []);
+
+ useEffect(() => {
+ const updateStatuses = (data) => {
+ statusManager1.updateStatuses(data);
+ };
+ updateStatuses(initialData);
+ }, [initialData]);
+
+ return {
+ nodes,
+ edges,
+ nodePositions,
+ setNodes,
+ setEdges,
+ onNodesChange,
+ onEdgesChange,
+ setNodePositions,
+ collapsedNodes,
+ toggleNodeCollapse,
+ initializeNodePositions
+ };
+};
diff --git a/src/Components/TreeChart/FlowChartComponents/useNodeHandlers.jsx b/src/Components/TreeChart/FlowChartComponents/useNodeHandlers.jsx
new file mode 100644
index 0000000..80e4fd0
--- /dev/null
+++ b/src/Components/TreeChart/FlowChartComponents/useNodeHandlers.jsx
@@ -0,0 +1,24 @@
+import { useCallback } from 'react';
+
+export const useNodeHandlers = (debouncedSetNodePositions) => {
+ const onNodeDrag = useCallback((event, node) => {
+ // Фиксируем позицию сразу при перемещении
+ node.position = {
+ x: Math.round(node.position.x),
+ y: Math.round(node.position.y)
+ };
+ }, []);
+
+ const onNodeDragStop = useCallback((event, node) => {
+ node.position = {
+ x: Math.round(node.position.x),
+ y: Math.round(node.position.y)
+ };
+ debouncedSetNodePositions(prev => ({
+ ...prev,
+ [node.id]: node.position
+ }));
+ }, [debouncedSetNodePositions]);
+
+ return { onNodeDrag, onNodeDragStop };
+};
\ No newline at end of file
diff --git a/src/Components/TreeChart/TreeChart.jsx b/src/Components/TreeChart/TreeChart.jsx
deleted file mode 100755
index dad7ed9..0000000
--- a/src/Components/TreeChart/TreeChart.jsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import React, { useRef, useEffect, useMemo, useState } from "react";
-import * as d3 from "d3";
-import { calculateNodePositions } from "./TreeChartComponents/NodePosition";
-import { getStatusColor } from "./dataUtils";
-
-const TreeChart = ({ data }) => {
- const chartRef = useRef();
- const nodePositions = useRef(new Map());
- const [treeData, setTreeData] = useState(data);
-
- // Пересчитываем позиции узлов при изменении данных
- const { root, nodes, links } = useMemo(() => {
- return calculateNodePositions(treeData, nodePositions.current);
- }, [treeData]);
-
- useEffect(() => {
- if (!chartRef.current) return;
-
- // Удаляем старый граф перед отрисовкой нового
- d3.select(chartRef.current).selectAll("*").remove();
-
- // Определяем границы графа
- const xMin = d3.min(nodes, (d) => d.x) || -500;
- const xMax = d3.max(nodes, (d) => d.x) || 500;
- const yMin = d3.min(nodes, (d) => d.y) || -500;
- const yMax = d3.max(nodes, (d) => d.y) || 500;
-
- const width = xMax - xMin + 200;
- const height = yMax - yMin + 200;
-
- const svg = d3.select(chartRef.current)
- .attr("width", "100%")
- .attr("height", "100%")
- .attr("viewBox", `${xMin - 100} ${yMin - 100} ${width} ${height}`)
- .attr("style", "max-width: 100%; height: auto;")
- .call(d3.zoom()
- .scaleExtent([0.5, 5])
- .on("zoom", (event) => {
- svg.select("g").attr("transform", event.transform);
- })
- );
-
- const g = svg.append("g");
-
- g.append("g").attr("class", "links");
- g.append("g").attr("class", "nodes");
- g.append("g").attr("class", "labels");
-
- // Рисуем связи
- g.select(".links")
- .selectAll("line")
- .data(links)
- .join("line")
- .attr("stroke", "#999")
- .attr("stroke-opacity", 0.6)
- .attr("x1", (d) => d.source.x)
- .attr("y1", (d) => d.source.y)
- .attr("x2", (d) => d.target.x)
- .attr("y2", (d) => d.target.y);
-
- // Рисуем узлы
- g.select(".nodes")
- .selectAll("circle")
- .data(nodes)
- .join("circle")
- .attr("fill", (d) => getStatusColor(d.data.status))
- .attr("stroke", "#fff")
- .attr("r", 7)
- .attr("cx", (d) => d.x)
- .attr("cy", (d) => d.y)
- .on("click", (event, d) => toggleNode(d))
- .call(drag());
-
- // Рисуем текстовые метки
- g.select(".labels")
- .selectAll("text")
- .data(nodes)
- .join("text")
- .text((d) => (nodes.length > 50 ? "" : d.data.title))
- .attr("dx", 12)
- .attr("dy", 4)
- .style("user-select", "none")
- .style("pointer-events", "none")
- .style("fill", "var(--TreeChart-text-color)")
- .attr("x", (d) => d.x + 12)
- .attr("y", (d) => d.y + 4);
-
- }, [root, links, nodes]);
-
- const drag = () => {
- function dragstarted(event, d) {
- d3.select(this).raise().attr("stroke", "#000");
- }
-
- function dragged(event, d) {
- d.x = event.x;
- d.y = event.y;
- d3.select(this).attr("cx", d.x).attr("cy", d.y);
-
- d3.select(this.parentNode)
- .select("text")
- .attr("x", d.x + 12)
- .attr("y", d.y + 4);
-
- d3.select(chartRef.current)
- .selectAll(".links line")
- .filter((link) => link.source === d || link.target === d)
- .attr("x1", (link) => link.source.x)
- .attr("y1", (link) => link.source.y)
- .attr("x2", (link) => link.target.x)
- .attr("y2", (link) => link.target.y);
- }
-
- function dragended(event, d) {
- d3.select(this).attr("stroke", "#fff");
- nodePositions.current.set(d.data.id, { x: d.x, y: d.y });
- }
-
- return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended);
- };
-
- const toggleNode = (d) => {
- d.data.collapsed = !d.data.collapsed;
-
- if (d.data.collapsed) {
- d._children = d.data.children;
- d.data.children = [];
- } else {
- d.data.children = d._children;
- d._children = null;
- }
-
- setTreeData({ ...treeData });
- };
-
- return ;
-};
-
-export default TreeChart;
diff --git a/src/Components/TreeChart/TreeChartComponents/Label.jsx b/src/Components/TreeChart/TreeChartComponents/Label.jsx
deleted file mode 100644
index 50fda20..0000000
--- a/src/Components/TreeChart/TreeChartComponents/Label.jsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from "react";
-
-const Label = ({ node }) => {
- return (
-
- {node.data.title}
-
- );
-};
-
-export default Label;
\ No newline at end of file
diff --git a/src/Components/TreeChart/TreeChartComponents/Link.jsx b/src/Components/TreeChart/TreeChartComponents/Link.jsx
deleted file mode 100644
index 653539a..0000000
--- a/src/Components/TreeChart/TreeChartComponents/Link.jsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from "react";
-
-const Link = ({ link }) => {
- return (
-
- );
-};
-
-export default Link;
\ No newline at end of file
diff --git a/src/Components/TreeChart/TreeChartComponents/Node.jsx b/src/Components/TreeChart/TreeChartComponents/Node.jsx
deleted file mode 100644
index 3291608..0000000
--- a/src/Components/TreeChart/TreeChartComponents/Node.jsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from "react";
-import * as d3 from "d3";
-import { getStatusColor } from "../dataUtils";
-
-const Node = ({ node, onNodeClick, drag }) => {
- return (
- drag(event, node)}
- onClick={() => onNodeClick(node.data.id, node.data.title)}
- />
- );
-};
-
-export default Node;
\ No newline at end of file
diff --git a/src/Components/TreeChart/TreeChartComponents/NodePosition.jsx b/src/Components/TreeChart/TreeChartComponents/NodePosition.jsx
deleted file mode 100644
index 2a7fd4d..0000000
--- a/src/Components/TreeChart/TreeChartComponents/NodePosition.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import * as d3 from "d3";
-
-export const calculateNodePositions = (data, nodePositions) => {
- if (!data || !data.items) return { root: null, nodes: [], links: [] };
-
- const root = d3.hierarchy(data, (d) => d.items);
- const treeLayout = d3.tree().size([4000 * Math.PI, 300]); // Угловое распределение (радиан, радиус)
-
- treeLayout(root); // Заполняем координаты
-
- const nodes = root.descendants();
- const links = nodes
- .filter((d) => d.parent)
- .map((d) => ({
- source: d.parent,
- target: d,
- }));
-
- // Преобразуем полярные координаты в декартовые
- nodes.forEach((node) => {
- const angle = node.x; // x теперь угол (радианы)
- const radius = node.y; // y теперь радиус
- nodePositions.set(node.data.id, {
- x: radius * Math.cos(angle),
- y: radius * Math.sin(angle),
- });
- });
-
- return { root, nodes, links };
-};
diff --git a/src/Components/UI/LoginModal.jsx b/src/Components/UI/LoginModal.jsx
index 98405a8..61c154d 100755
--- a/src/Components/UI/LoginModal.jsx
+++ b/src/Components/UI/LoginModal.jsx
@@ -1,6 +1,7 @@
import React, { useState } from "react";
import Modal from "./Modal";
import "../../Style/LoginModal.css";
+import Logo from '../../assets/images/logo.svg?react';
import TextField from '@mui/material/TextField';
const LoginModal = ({ onLogin, onClose }) => {
diff --git a/src/Components/hooks/TabContent.jsx b/src/Components/hooks/TabContent.jsx
index 5747736..10e58a0 100644
--- a/src/Components/hooks/TabContent.jsx
+++ b/src/Components/hooks/TabContent.jsx
@@ -1,6 +1,6 @@
import SystemStatusChart from "../../Charts/SystemStatusChart";
import TreeTable from "../UI/TreeTable";
-import TreeChart from "../TreeChart/TreeChart";
+
import FlowChart from "../TreeChart/FlowChart";
const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {
diff --git a/src/assets/images/logo.svg b/src/assets/images/logo.svg
new file mode 100644
index 0000000..c767d1c
--- /dev/null
+++ b/src/assets/images/logo.svg
@@ -0,0 +1,319 @@
+
+
+
diff --git a/vite.config.js b/vite.config.js
index b034150..7011d63 100755
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,11 +1,15 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+import svgr from 'vite-plugin-svgr'
// https://vite.dev/config/
export default defineConfig({
- plugins: [react()],
+ plugins: [
+ react(),
+ svgr()
+ ],
server: {
host: true,
allowedHosts: ['dev.msf.enode']
}
-})
+})
\ No newline at end of file