【React】React+Pythonでマイクロサービスの依存関係を可視化してみた

# やりたいこと
私の会社ではマイクロサービスアーキテクチャを採用している。
そこでサービス同士の複雑な依存関係を可視化してみたい。
※企業秘密の観点からサービス名はハッシュ値に置換してある。
![service.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/377231/76e93a67-d134-0170-96cf-721ce1eb0715.gif)

# 要件
- 閲覧したいサービスの依存関係を矢印の向きとノードの色、線の種類（点線は切れても致命的ではない関係性）で表現したい。
- サービスを検索できるようにしたい。
- ノードがクリックされたらそのノードのサービスにまつわる依存関係を表示させたい。


# システム
![スクリーンショット 2023-11-26 21.34.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/377231/5bb96a14-8f7a-6f39-8ac1-a849bb040c1c.png)
現在表示されたものをキープしたまま追加でクリックされたノードにまつわる関係性を描写するために非同期処理が必要。よってフロントアプリとしてReactを用いる。正直Pythonなくても実現可能なのだが、私がReactが初めてのため、React側が欲しい形を可能な限りPyhton側で処理して渡したいのでバックエンドアプリとしてPythonを用いた。


# Python
フレームワークとしてflaskを用いた。
データの取得元が社内のAPIサービスというのもあり、秘匿的な観点からPython部分は割愛する。（Qiita用にAPIレスポンス受信後のコードを編集するのが面倒😅）

ちなみにPython側からReact側に返すJSONはこんな感じ
例：サービスID`314`に関係する依存関係を取得したい場合
```:request
http://pythonアプリのエンドポイント/react/314
```

```json:response
{
"edge_array": [
[
"314",
"358",
1
],
[
"314",
"926",
1
],
[
"314",
"935",
0
],
[
"314",
"1022",
0
],
[
"314",
"2435",
1
],
[
"63",
"314",
1
],
[
"144",
"314",
1
],
[
"222",
"314",
1
],
[
"345",
"314",
0
],
[
"351",
"314",
0
],
[
"352",
"314",
0
],
[
"533",
"314",
1
],
[
"536",
"314",
1
],
[
"551",
"314",
1
],
[
"748",
"314",
0
],
[
"802",
"314",
0
],
[
"807",
"314",
0
],
[
"808",
"314",
0
],
[
"1279",
"314",
0
],
[
"1488",
"314",
0
],
[
"1607",
"314",
0
],
[
"1614",
"314",
0
],
[
"1745",
"314",
0
],
[
"1851",
"314",
0
],
[
"2074",
"314",
1
],
[
"2353",
"314",
0
],
[
"2499",
"314",
0
],
[
"2580",
"314",
0
],
[
"2856",
"314",
0
],
[
"2932",
"314",
0
],
[
"2934",
"314",
0
]
],
"node_array": [
[
"314",
"314",
1
],
[
"358",
"358",
2
],
[
"926",
"926",
2
],
[
"935",
"935",
2
],
[
"1022",
"1022",
2
],
[
"2435",
"2435",
2
],
[
"63",
"63",
3
],
[
"144",
"144",
3
],
[
"222",
"222",
3
],
[
"345",
"345",
3
],
[
"351",
"351",
3
],
[
"352",
"352",
3
],
[
"533",
"533",
3
],
[
"536",
"536",
3
],
[
"551",
"551",
3
],
[
"748",
"748",
3
],
[
"802",
"802",
3
],
[
"807",
"807",
3
],
[
"808",
"808",
3
],
[
"1279",
"1279",
3
],
[
"1488",
"1488",
3
],
[
"1607",
"1607",
3
],
[
"1614",
"1614",
3
],
[
"1745",
"1745",
3
],
[
"1851",
"1851",
3
],
[
"2074",
"2074",
3
],
[
"2353",
"2353",
3
],
[
"2499",
"2499",
3
],
[
"2580",
"2580",
3
],
[
"2856",
"2856",
3
],
[
"2932",
"2932",
3
],
[
"2934",
"2934",
3
]
]
}
```
二つのオブジェクト"node_array"と"edge_array"を返している。

"node_array"はノードのリストである
```json
"node_array": [["ノード", "ラベル",　グループ], ...]
```
ラベルの箇所をこのQiita上ではノードと同じ番号記載にしてある。
本来はここに実際のサービス名がくるので各ノードはサービス名で表示される。
グループを指定すると同じグループ同士のノードの色を同じにすることができる。クリックされたサービスを橙色、クリックされたサービスが依存しているサービスを青色、クリックされたサービスに依存しているサービスを水色で表現した。

"edge_array"はノードとノードを結ぶ線のためのリストである
```json
"edge_array": [["ノードA","ノードB",1],["ノードB","ノードC",0]
```
例えばこの場合はノードAからノードBに対して矢印が実践で接続され、ノードBからノードCに対して点線で矢印が接続される。


# React
描写にVis.jsを利用。このVis.jsモジュールにノードオブジェクトとエッジオブジェクトを渡して描写している。
```react:index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import GraphVisualization from './GraphVisualization';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <React.StrictMode>
    <GraphVisualization />
  </React.StrictMode>
);

reportWebVitals();
```


```react:GraphVisualization.js
import React, { Component } from 'react';
import { DataSet, Network } from 'vis';
import axios from 'axios';
import Template from './Template';

class GraphVisualization extends Component {
    constructor(props) {
        super(props);
        this.state = 
            nodes: new DataSet([]),
            edges: new DataSet([]),
            clickedUnicornIdList: [],
        };
    }


    // エッジの中身を変更する処理
    updateEdgeContent = (nodeA, nodeB) => {

        const edgesToRemove = this.state.edges.get({
            filter: (edge) => (edge.from === nodeA && edge.to === nodeB) || (edge.from === nodeB && edge.to === nodeA)
        });

        edgesToRemove.forEach((edge) => {
            const edgeIdToRemove = edge.id;
            edge.length = 1500
            this.state.edges.update({ from: edge.from, to: edge.to, length: 1500 });
        });

        this.setState({ edges: this.state.edges });

    }

    // ノードがクリックされた場合の処理
    handleNodeClick = async (event) => {
        if (event.nodes.length > 0) {
            const clickedNodeId = event.nodes[0];
            const clickedNode = this.state.nodes.get(clickedNodeId);

            try {
                const response = await axios.get(`http://Python側のエンドポイント/react/${clickedNode.id}`);
                const nodeArray = response.data.node_array;

                for (const node of nodeArray) {
                    const nodeId = node[0];
                    const nodeName = node[1];
                    const group = node[2];

                    const isNodeIdIncluded = this.state.nodes.get().some(node => node.id === nodeId);

                    if (!isNodeIdIncluded) {
                        if (group == 2) {
                            this.state.nodes.add({ id: nodeId, label: nodeName, color: { background: 'paleturquoise', border: 'paleturquoise', highlight: { background: 'darkkhaki', border: 'darkkhaki' } } }); // 例: 新しいノードを追加
                        } else {
                            this.state.nodes.add({ id: nodeId, label: nodeName, color: { background: 'lightsteelblue', border: 'lightsteelblue', highlight: { background: 'darkkhaki', border: 'darkkhaki' } } }); // 例: 新しいノードを追加
                        }
                    }
                }

                const edgeArray = response.data.edge_array;
                for (const edge of edgeArray) {
                    const from = edge[0];
                    const to = edge[1];
                    const lineType = edge[2];

                    function findEdgeByFromTo(edges, fromNodeId, toNodeId) {
                        return edges.get().find(edge => edge.from === fromNodeId && edge.to === toNodeId);
                    }

                    const foundEdge = findEdgeByFromTo(this.state.edges, from, to);

                    if (!foundEdge) {
                        if (lineType === 1) {
                            this.state.edges.add({ from: from, to: to, arrows: 'to', dashes: true, label: 'No Effect from Disconnection', color: { highlight: 'red' } });
                        } else {
                            this.state.edges.add({ from: from, to: to, arrows: 'to', color: 'bule', color: { highlight: 'red' } });
                        }
                    }
                }

                // 一個前にクリックされたノードを取得
                const last = this.state.clickedUnicornIdList.slice(-1)[0];

                // ノードの中身を変更
                // console.log(last, clickedNode.id)
                this.updateEdgeContent(last, clickedNode.id);

                // ステートを更新して再レンダリング
                this.setState({ nodes: this.state.nodes, edges: this.state.edges });

                // クリックされたノードIDをリストの末尾に追加
                this.state.clickedUnicornIdList.push(clickedNode.id);

            } catch (error) {
                //console.error('Error while making the GET request:', error);
            }
        }
    };

    componentDidMount() {

        this.options = {
            physics: {
                barnesHut: {
                    springLength: 200, // ノード間の距離
                    springConstant: 0.2, // ノード間のスプリングの強度
                    damping: 0.2, // ダンピング係数
                    avoidOverlap: 0.2, // ノードが重ならないように調整
                },
            },

            nodes: {
                //shape: "dot",
                size: 10,
                font: {
                    color: 'black',
                },
                color: "black"
            },
            edges: {
                arrows: 'to',
                smooth: false
            },
        };
        const container = document.getElementById('graph-container');
        this.network = new Network(container, this.state, this.options);
        this.network.on('click', this.handleNodeClick);
    }

    render() {
        return (
            <div>
                <Template />
                <div className="center">
                    <div className="form-group">
                        <select name="unitid" required onChange={(event) => {
                            const word = event.target.value;
                        }}>
                            // 検索欄に表示させたいサービス群
                            <option value="1">サービスA</option>
                            <option value="314">サービスB</option>
                            <option value="3223">サービスC</option>
                            <option value="3633">サービスD</option>
                            <option value="7">サービスE</option>
                            <option value="1510">サービスF</option>
                            <option value="1279">サービスG</option>
                            <option value="1806">サービスH</option>
                        </select>

                        <button onClick={async () => {
                            const word = document.querySelector('[name="unitid"]').value;
                            const res = await fetch(`http://Python側のエンドポイント/react/${word}`, { method: "GET" });
                            const json = await res.json();
                            const nodeArray = json.node_array;
                            const edgeArray = json.edge_array;

                            for (const node of nodeArray) {
                                const nodeId = node[0];
                                const nodeName = node[1];
                                const group = node[2];

                                const isNodeIdIncluded = this.state.nodes.get().some(node => node.id === nodeId);

                                if (!isNodeIdIncluded) {
                                    if (group == 2) {
                                        if (nodeId == word) {
                                            this.state.nodes.add({ id: nodeId, label: nodeName, color: { background: 'darkkhaki', border: 'darkkhaki', highlight: { background: 'gray', border: 'gray' } } });
                                        } else {
                                            this.state.nodes.add({ id: nodeId, label: nodeName, color: { background: 'paleturquoise', border: 'paleturquoise', highlight: { background: 'darkkhaki', border: 'darkkhaki' } } }); // 例: 新しいノードを追加
                                        }
                                    } else {
                                        if (nodeId == word) {
                                            this.state.nodes.add({ id: nodeId, label: nodeName, color: { background: 'darkkhaki', border: 'darkkhaki', highlight: { background: 'gray', border: 'gray' } } });
                                        } else {
                                            this.state.nodes.add({ id: nodeId, label: nodeName, color: { background: 'lightsteelblue', border: 'lightsteelblue', highlight: { background: 'darkkhaki', border: 'darkkhaki' } } }); // 例: 新しいノードを追加
                                        }
                                    }
                                }
                            }

                            for (const edge of edgeArray) {
                                const from = edge[0];
                                const to = edge[1];
                                const lineType = edge[2];

                                function findEdgeByFromTo(edges, fromNodeId, toNodeId) {
                                    return edges.get().find(edge => edge.from === fromNodeId && edge.to === toNodeId);
                                }

                                const foundEdge = findEdgeByFromTo(this.state.edges, from, to);

                                if (!foundEdge) {
                                    if (lineType === 1) {
                                        this.state.edges.add({ from: from, to: to, arrows: 'to', dashes: true, label: 'No Effect from Disconnection', color: { highlight: 'red' } });
                                    } else {
                                        this.state.edges.add({ from: from, to: to, arrows: 'to', color: 'bule', color: { highlight: 'red' } });
                                    }
                                }
                            }
                            this.setState({ nodes: this.state.nodes, edges: this.state.edges });
                        }}>see</button>
                        <button onClick={() => {
                            const emptyNodes = new DataSet([]);
                            const emptyEdges = new DataSet([]);

                            this.setState({
                                nodes: emptyNodes,
                                edges: emptyEdges,
                            }, () => {
                                // setStateの完了後にページを再読み込み
                                window.location.reload();
                            });
                        }}>clear</button>
                    </div>
                </div>
                <div className="center">
                    <div id="graph-container" style={{ width: '2500px', height: '1200px' }} />
                </div>
            </div>
        );
    }
}

export default GraphVisualization;
```

各種CSS、publicディレクトリの中身は割愛。

初めてのReactだった。
```
npx create-react-app {ディレクトリ名}
```
でサクッと作れて
```
npm start  
```
でサクッと実行できるのはいいですね。