[#Node-RED]：flows.jsonのノード一覧（固有URL対応版）

# 使えるぞ！Node-RED3.10の固有URL
正式版が待ち遠しいNode-RED3.10。ノードやグループに固有のURLに対応しました。
これまでだと、フロー中のコメントノードなどに手順を書く際に「XXタブの上から3つ目の[実行]と書かれたインジェクトノードのボタンを押下してください。」と場所を説明する必要がありましたがが、3.10だと「[ここ](https://qiita.com/sakaba37/items/14215a7d87317a8f67f3)のインジェクトノードのボタンを押してください」とリンク付きで説明できます。これができるとコードと同時にドキュメントを管理でき、ドキュメントとコードの不一致も少なくなると期待できます。
この固有のURLはNode-RED3.10では情報サイドバーの「要素のURLをコピー」ボタンで取得できますが、リンクをコメントノードに貼ろうとすると、編集中にエディタ内を行ったり来たりすることになります。そこで、以前作成した[ノード一覧フロー](https://qiita.com/sakaba37/items/14215a7d87317a8f67f3 "[#Node-RED]flows.jsonからノード一覧を作成する")をノード、グループ、タブに固有のURLを付加しました。いくつかの不具合修正のほか、一覧作成フローのタブを表示しない、タブごとに見出しを追加する、などの機能追加もしています。
![NodeList202308a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147007/68bccea5-8ad7-f9d6-f5e5-5dc6738c7acf.png)

# 使い方は2種類
コードを下につけていますので、Node-REDで読み込んで使ってください。ブラウザでNode-REDのURLの下のlist-localまたはlist-jsonを参照してください。list-localは~/.node-red/flows.jsonを、list-jsonはテンプレートノードにペーストしたflows.jsonを参照します。なお、flows.jsonは少なくとも一つのタブが必要です。
3.10の正式版はこの記事作成時点でまだ出てません。なかなか出てこないのでベータ4版で開発しました。利用される場合は、@nextをつけてインストールしてください。
　　　$ sudo npm install -g --unsafe-perm node-red@next

# 参考文献
[[#Node-RED]flows.jsonのフォーマットを見てみる](https://qiita.com/sakaba37/items/88366b65851ac7f01048)
[[#Node-RED]flows.jsonからノード一覧を作成する](https://qiita.com/sakaba37/items/14215a7d87317a8f67f3)

# コード

<div class="highlight"><pre><code>
[{"id":"970e4ed7b944a9b6","type":"tab","label":"__NodeList__","disabled":false,"info":"flows.jsonのノード一覧を作成します。","env":[]},{"id":"86f312d94d774b20","type":"junction","z":"970e4ed7b944a9b6","x":580,"y":220,"wires":[["ac03cf7426581af1"]]},{"id":"d47fc116556c1d98","type":"debug","z":"970e4ed7b944a9b6","name":"HTML","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":430,"y":660,"wires":[]},{"id":"fdbe3d70e31de3c9","type":"file in","z":"970e4ed7b944a9b6","name":"","filename":"filename","filenameType":"msg","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":480,"y":220,"wires":[["86f312d94d774b20"]]},{"id":"89278eb590ee4d16","type":"function","z":"970e4ed7b944a9b6","name":"~/.node-red/flows.json","func":"msg.filename = msg.payload + '/.node-red/flows.json'\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":300,"y":220,"wires":[["fdbe3d70e31de3c9"]]},{"id":"ac03cf7426581af1","type":"json","z":"970e4ed7b944a9b6","name":"","property":"payload","action":"","pretty":false,"x":170,"y":340,"wires":[["db8d0c075506ec6b"]]},{"id":"db8d0c075506ec6b","type":"function","z":"970e4ed7b944a9b6","name":"タブ一覧)(__NodeList__削除)","func":"msg.objs = msg.payload;\nmsg.payload = msg.objs.filter(elm => elm.type === 'tab');\n// msg.payload = msg.objs.filter(elm => (elm.type === 'tab') && (elm.label !='__NodeList__'));\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":340,"wires":[["9deebe2b3a79aefe"]]},{"id":"9deebe2b3a79aefe","type":"split","z":"970e4ed7b944a9b6","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":230,"y":440,"wires":[["3bea992d98e4d4d5"]]},{"id":"a24f81853c9f22b8","type":"join","z":"970e4ed7b944a9b6","name":"","mode":"auto","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":"false","timeout":"","count":"","reduceRight":false,"x":230,"y":500,"wires":[["d8cbbdc08d6d18d5"]]},{"id":"3bea992d98e4d4d5","type":"function","z":"970e4ed7b944a9b6","name":"解析とマークダウン作成","func":"const tab_path = '/#flow/'\nconst node_path = '/#node/'\nconst group_path = '/#group/'\n// settings.jsのhttpAdminRootを'/admin'に修正した場合\n// const tab_path = '/admin/#flow/'\n// const node_path = '/admin/#node/'\n// const group_path = '/admin/#group/'\n\n// 表が壊れるので説明のマークダウンをHTMLにする\nfunction conv(str) {\n    // if (!str) return '';\n    let md = markdownIt({ html: true, linkify: true, typographer: true });\n    return (md.render(str).trim().replace(/\\n/g, '<br>'));\n}\n\n// 説明欄の文字列作成 disabled mode infoの結合(見たいプロパティはここに追記)\nfunction mkRem(obj) {\n    return (obj.disabled ? 'disabled ' : '')+\n        (obj.d ? 'disabled ' : '') +\n        (obj.mode ? ('mode:'+obj.mode+' ') : '')+\n        (obj.info ? conv(obj.info) : '');\n}\n\nlet tab = msg.payload;\n\n// タブ内のオブジェクト抽出\nlet objs = msg.objs.filter(elm => elm.z === tab.id);\n\n// 抽出するノードの「種類を限定する場合はさらにフィルタする\n// objs = objs.filter(elm => elm.type === 'comment');\n\n// オブジェクトの位置ソート\nobjs.sort((a, b) => {\n    if (a.y == b.y) return a.x - b.x;\n    else return a.y - b.y;\n});\n\n/// グループのMAP作成（リンクノード分析は全体に対して行う）\nlet groups = objs.filter(elm => elm.type === 'group');\nlet groupName = {};\ngroups.forEach(group => { groupName[group.id] = group.name ? group.name : '-'+group.id+'-'});\n\nmsg.payload = '';\n\n\n// タブマークダウン\nmsg.payload += '\\n| **[```' + tab.label +'```]('+tab_path+tab.id+')** | **' +tab.type+'** |  | '+mkRem(tab)+' |';\n\n// オブジェクトマークダウン\nobjs.forEach(obj => {\n    msg.payload += '\\n| [```' + (obj.name === '' || !obj.name ? '-' + obj.id + '-' : obj.name) + \n        '```](' + (obj.type === 'group' ? group_path : node_path) + obj.id + ') | ' + obj.type + ' | ' + \n        (obj.g ? groupName[obj.g] : '') + ' | ' + mkRem(obj) + ' |';\n});\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[{"var":"markdownIt","module":"markdown-it"}],"x":430,"y":440,"wires":[["a24f81853c9f22b8"]]},{"id":"5c8d3dd566715bc3","type":"comment","z":"970e4ed7b944a9b6","name":"タブごとの情報の作成","info":"**split**+**join**を用いてタブ単位で並行処理しています。","x":140,"y":400,"wires":[]},{"id":"134559eded4d1e6c","type":"template","z":"970e4ed7b944a9b6","name":"ここにJSONをペーストする","field":"payload","fieldType":"msg","format":"json","syntax":"plain","template":"","output":"str","x":380,"y":100,"wires":[["86f312d94d774b20"]]},{"id":"d8cbbdc08d6d18d5","type":"function","z":"970e4ed7b944a9b6","name":"表をまとめる","func":"// ヘッダマークダウン追加\n// msg.payload.unshift(`| 名前 | 種類 |  グループ | 説明 |\\n| ---- | ---- | ---- | ---- |`);\n\n// 表の結合\nmsg.payload = `| Name | Type | Group | Detail |\\n| ---- | ---- | ---- | ---- |` +\n        msg.payload.join('\\n| **Name** | **Type** | **Group** | **Detail** |');\n// msg.payload = `| 名前 | 種類 |  グループ | 説明 |\\n| ---- | ---- | ---- | ---- |` +\n//         msg.payload.join('\\n| **名前** | **種類** | **グループ** | **説明** |');\n// msg.payload = msg.payload.join('');\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":600,"wires":[["62a050f4f4e791b1","b1c87663ee56f99b"]]},{"id":"b1c87663ee56f99b","type":"markdown","z":"970e4ed7b944a9b6","name":"","x":230,"y":700,"wires":[["d47fc116556c1d98","69031c4cfc3d0fb5"]]},{"id":"49d8107f50af82f0","type":"http in","z":"970e4ed7b944a9b6","name":"","url":"/list-local","method":"get","upload":false,"swaggerDoc":"","x":150,"y":160,"wires":[["eb2cac95ebbde58a"]]},{"id":"eb2cac95ebbde58a","type":"change","z":"970e4ed7b944a9b6","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"HOME","tot":"env"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":160,"wires":[["89278eb590ee4d16"]]},{"id":"d7af628eac5c00e4","type":"http in","z":"970e4ed7b944a9b6","name":"","url":"/list-json","method":"get","upload":false,"swaggerDoc":"","x":150,"y":100,"wires":[["134559eded4d1e6c"]]},{"id":"29a1f1043ae1c1ea","type":"http response","z":"970e4ed7b944a9b6","name":"","statusCode":"","headers":{},"x":590,"y":700,"wires":[]},{"id":"69031c4cfc3d0fb5","type":"function","z":"970e4ed7b944a9b6","name":"ヘッダー追加","func":"msg.payload = msg.payload.replace('<table>', '<table class=\"table table-bordered\">');\n\nmsg.payload =\n    `<html>\n<head>\n    <title>Node List</title>\n    <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC\" crossorigin=\"anonymous\">\n</head>\n<body>\n<h1>Node List</h1>\n<div class=”container”>\n  <div class=”row”>\n    <div class=\"p-3\"> ` +\n    msg.payload +\n    `   </div>\n  </div>\n</div>\n</body>\n</html>`\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":700,"wires":[["29a1f1043ae1c1ea"]]},{"id":"62a050f4f4e791b1","type":"debug","z":"970e4ed7b944a9b6","name":"マークダウン","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":540,"y":600,"wires":[]},{"id":"94a234e2bbab804e","type":"comment","z":"970e4ed7b944a9b6","name":"データ入力","info":"ブラウザから以下のいずれかをを参照してください。\n- http(s)://ホスト:ポート/list-json\n- http(s)://ホスト:ポート/list-local","x":100,"y":60,"wires":[]},{"id":"4994573e34e9ede4","type":"comment","z":"970e4ed7b944a9b6","name":"全体で使う情報の作成","info":"","x":140,"y":280,"wires":[]},{"id":"793d31ab83d60a8b","type":"comment","z":"970e4ed7b944a9b6","name":"全てのタブ情報の結合","info":"","x":140,"y":560,"wires":[]},{"id":"5dc2829793d8a062","type":"comment","z":"970e4ed7b944a9b6","name":"HTML作成","info":"","x":100,"y":660,"wires":[]},{"id":"37890012798eda13","type":"comment","z":"970e4ed7b944a9b6","name":"要修正：httpAdminRoot設定時","info":"","x":590,"y":400,"wires":[]}]
</code></pre></div>