├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── Logo-large.png ├── Logo.png ├── code-agate.css ├── config.js ├── icons │ ├── cluster.png │ ├── configmap.png │ ├── container.png │ ├── cronjob.png │ ├── daemonset.png │ ├── deployment.png │ ├── endpoints.png │ ├── ingress.png │ ├── job.png │ ├── k8s.png │ ├── networkpolicy.png │ ├── persistentvolume.png │ ├── persistentvolumeclaim.png │ ├── pod.png │ ├── replicaset.png │ ├── replicationcontroller.png │ ├── secret.png │ ├── service.png │ ├── skydive-logo-16x16.png │ ├── statefulset.png │ └── storageclass.png ├── nsconfig.js ├── options.js ├── tools.js └── topology.css ├── entry.sh ├── package-lock.json ├── package.json ├── samples ├── datacenter.json ├── kubernetes.json ├── ocp.json ├── ocp_istio.json ├── scale.json ├── simple.json └── submariner.json ├── screenshot.png ├── src ├── About.tsx ├── ActionButtons │ ├── Capture.tsx │ └── Gremlin.tsx ├── App.css ├── App.tsx ├── AppStyles.ts ├── AutoComplete.tsx ├── AutoCompleteStyles.ts ├── Config.ts ├── DataPanels │ ├── Capture.tsx │ ├── CaptureForm.tsx │ ├── CaptureFormStyles.ts │ ├── CaptureStyles.ts │ ├── Flow.tsx │ ├── FlowStyles.ts │ ├── Gremlin.tsx │ ├── GremlinStyles.ts │ ├── Panel.tsx │ └── PanelStyles.ts ├── Login.tsx ├── LoginStyles.ts ├── Menu.tsx ├── Png.d.ts ├── SelectionPanel.tsx ├── SelectionPanelStyles.ts ├── StdDataNormalizer.ts ├── StdDataPanel.css ├── StdDataPanel.tsx ├── StdDataPanelStyles.ts ├── StdDataViewer.css ├── StdDataViewer.tsx ├── Store.ts ├── Tabs.tsx ├── TimetravelPanel.tsx ├── TimetravelPanelStyles.ts ├── Tools.ts ├── Topology.css ├── Topology.tsx ├── api │ ├── api.ts │ ├── configuration.ts │ ├── custom.d.ts │ └── index.ts ├── index.css ├── index.html └── index.tsx ├── tests └── DataNormalizer.test.ts ├── tools └── csvstoskyui │ ├── README.md │ ├── csvstoskyui.py │ ├── data │ ├── example_datacenter │ │ ├── applications.csv │ │ ├── config.js │ │ ├── data_centers.csv │ │ ├── dump.conf │ │ ├── hosts.csv │ │ └── vms.csv │ ├── example_multijson │ │ ├── DataCenter1.conf │ │ ├── DataCenter2.conf │ │ ├── applications.csv │ │ ├── config.js │ │ ├── data_centers.csv │ │ ├── dump.conf │ │ ├── hosts.csv │ │ └── vms.csv │ ├── example_scale │ │ ├── applications.csv │ │ ├── config.js │ │ ├── data_centers.csv │ │ ├── dump.conf │ │ ├── hosts.csv │ │ └── vms.csv │ └── example_submariner │ │ ├── Config.ts │ │ ├── brokers.csv │ │ ├── clusters.csv │ │ ├── dump.conf │ │ ├── nodes.csv │ │ ├── pods.csv │ │ └── topology.css │ ├── do.sh │ └── images │ └── readme_screenshot.jpg ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | skydivetopology.json 12 | 13 | # misc 14 | .DS_Store 15 | .env 16 | .idea/ 17 | out/ 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM skydive/skydive 2 | 3 | WORKDIR /usr/src/skydive-ui 4 | 5 | RUN apt-get -y update \ 6 | && apt-get -y install nodejs npm 7 | 8 | COPY package.json /usr/src/skydive-ui 9 | COPY package-lock.json /usr/src/skydive-ui 10 | COPY tsconfig.json /usr/src/skydive-ui 11 | COPY webpack.config.js /usr/src/skydive-ui 12 | COPY assets /usr/src/skydive-ui/assets 13 | COPY src /usr/src/skydive-ui/src 14 | COPY entry.sh /usr/src/skydive-ui 15 | 16 | RUN npm install 17 | RUN npm run build 18 | 19 | ENTRYPOINT /usr/src/skydive-ui/entry.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub license](https://img.shields.io/badge/license-Apache%20license%202.0-blue.svg)](https://github.com/skydive-project/skydive-ui/blob/master/LICENSE) 2 | [![Slack Invite](https://img.shields.io/badge/Slack:-%23skydive‐project%20invite-blue.svg?style=plastic&logo=slack)](https://slack.skydive.network) 3 | [![Slack Channel](https://img.shields.io/badge/Slack:-%23skydive‐project-blue.svg?style=plastic&logo=slack)](https://skydive-project.slack.com) 4 | 5 | # Next generation Skydive WebUI 6 | 7 | Please note this version of the Skydive WebUI is still actively developed. 8 | Not all the features of the legacy version are implemented yet. 9 | 10 | ![](https://raw.githubusercontent.com/skydive-project/skydive-ui/master/screenshot.png) 11 | 12 | ## Live demo 13 | 14 | * [1 TOR, 2 hosts, 2 namespace per host topology](https://skydive-project.github.io/skydive-ui/?data=/skydive-ui/assets/simple.json) 15 | * [Minikube topology](https://skydive-project.github.io/skydive-ui/?data=/skydive-ui/assets/kubernetes.json) 16 | 17 | ## Quick start 18 | 19 | A docker image is available with the latest version. Please note that this image 20 | is currently provided for testing purpose. 21 | 22 | ``` 23 | docker run -p 8080:8080 skydive/skydive-ui 24 | ``` 25 | 26 | By default the WebUI is trying to connect to a working skydive analyser. Make sure that the analyser is available to the WebUI on localhost:8082 27 | 28 | Note: To use different skydive analyzer end-point you need to logout (top-right icon) and select a different end-point on the login screen 29 | 30 | To experience with static kubernetes example open your browser with the following address 31 | 32 | ``` 33 | http://127.0.0.1:8080/?data=/samples/kubernetes.json 34 | ``` 35 | 36 | In order to load an example of local topology dump 37 | 38 | ``` 39 | docker run -p 8080:8080 -v dump.json:/usr/src/skydive-ui/assets/dump.json skydive/skydive-ui 40 | ``` 41 | 42 | then open your browser with the following address 43 | 44 | ``` 45 | http://127.0.0.1:8080/?data=/assets/dump.json 46 | ``` 47 | 48 | ## Dev mode 49 | 50 | ``` 51 | npm install 52 | npm start 53 | ``` 54 | 55 | In order to load a local topology dump 56 | 57 | ``` 58 | PAGE="?data=/assets/dump.json" npm start 59 | ``` 60 | 61 | ## Get involved 62 | 63 | * Slack 64 | * Invite : https://slack.skydive.network 65 | * Workspace : https://skydive-project.slack.com 66 | 67 | ## Contributing 68 | 69 | Your contributions are more than welcome. Please check 70 | https://github.com/skydive-project/skydive/blob/master/CONTRIBUTING.md 71 | to know about the process. 72 | 73 | ## License 74 | 75 | This software is licensed under the Apache License, Version 2.0 (the 76 | "License"); you may not use this software except in compliance with the 77 | License. 78 | You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 79 | 80 | Unless required by applicable law or agreed to in writing, software 81 | distributed under the License is distributed on an "AS IS" BASIS, 82 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 83 | See the License for the specific language governing permissions and 84 | limitations under the License. 85 | -------------------------------------------------------------------------------- /assets/Logo-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/Logo-large.png -------------------------------------------------------------------------------- /assets/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/Logo.png -------------------------------------------------------------------------------- /assets/code-agate.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Agate by Taufik Nurrohman 3 | * ---------------------------------------------------- 4 | * 5 | * #ade5fc 6 | * #a2fca2 7 | * #c6b4f0 8 | * #d36363 9 | * #fcc28c 10 | * #fc9b9b 11 | * #ffa 12 | * #fff 13 | * #333 14 | * #62c8f3 15 | * #888 16 | * 17 | */.hljs{display:block;overflow-x:auto;padding:.5em;border-radius:4px;}.hljs-name,.hljs-strong{font-weight:bold}.hljs-code,.hljs-emphasis{font-style:italic}.hljs-tag{color:#62c8f3}.hljs-variable,.hljs-template-variable,.hljs-selector-id,.hljs-selector-class{color:#ade5fc}.hljs-string,.hljs-bullet{color:#a2fca2}.hljs-type,.hljs-title,.hljs-section,.hljs-attribute,.hljs-quote,.hljs-built_in,.hljs-builtin-name{color:#ffa}.hljs-number,.hljs-symbol,.hljs-bullet{color:#d36363}.hljs-keyword,.hljs-selector-tag,.hljs-literal{color:#fcc28c}.hljs-comment,.hljs-deletion,.hljs-code{color:#888}.hljs-regexp,.hljs-link{color:#c6b4f0}.hljs-meta{color:#fc9b9b}.hljs-deletion{background-color:#fc9b9b;color:#333}.hljs-addition{background-color:#a2fca2;color:#333}.hljs a{color:inherit}.hljs a:focus,.hljs a:hover{color:inherit;text-decoration:underline} -------------------------------------------------------------------------------- /assets/config.js: -------------------------------------------------------------------------------- 1 | config = { 2 | } -------------------------------------------------------------------------------- /assets/icons/cluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/cluster.png -------------------------------------------------------------------------------- /assets/icons/configmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/configmap.png -------------------------------------------------------------------------------- /assets/icons/container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/container.png -------------------------------------------------------------------------------- /assets/icons/cronjob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/cronjob.png -------------------------------------------------------------------------------- /assets/icons/daemonset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/daemonset.png -------------------------------------------------------------------------------- /assets/icons/deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/deployment.png -------------------------------------------------------------------------------- /assets/icons/endpoints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/endpoints.png -------------------------------------------------------------------------------- /assets/icons/ingress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/ingress.png -------------------------------------------------------------------------------- /assets/icons/job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/job.png -------------------------------------------------------------------------------- /assets/icons/k8s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/k8s.png -------------------------------------------------------------------------------- /assets/icons/networkpolicy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/networkpolicy.png -------------------------------------------------------------------------------- /assets/icons/persistentvolume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/persistentvolume.png -------------------------------------------------------------------------------- /assets/icons/persistentvolumeclaim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/persistentvolumeclaim.png -------------------------------------------------------------------------------- /assets/icons/pod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/pod.png -------------------------------------------------------------------------------- /assets/icons/replicaset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/replicaset.png -------------------------------------------------------------------------------- /assets/icons/replicationcontroller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/replicationcontroller.png -------------------------------------------------------------------------------- /assets/icons/secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/secret.png -------------------------------------------------------------------------------- /assets/icons/service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/service.png -------------------------------------------------------------------------------- /assets/icons/skydive-logo-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/skydive-logo-16x16.png -------------------------------------------------------------------------------- /assets/icons/statefulset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/statefulset.png -------------------------------------------------------------------------------- /assets/icons/storageclass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/icons/storageclass.png -------------------------------------------------------------------------------- /assets/nsconfig.js: -------------------------------------------------------------------------------- 1 | config = { 2 | subTitle: "dynamic config", 3 | defaultFilter: "namespaces", 4 | } -------------------------------------------------------------------------------- /assets/options.js: -------------------------------------------------------------------------------- 1 | var baseURL = "/"; -------------------------------------------------------------------------------- /assets/tools.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/assets/tools.js -------------------------------------------------------------------------------- /assets/topology.css: -------------------------------------------------------------------------------- 1 | .host .node-hexagon { 2 | fill: #114B5F; 3 | background-color: #114B5F; 4 | } 5 | 6 | .netns .node-hexagon { 7 | fill: #C6DABF; 8 | background-color: #C6DABF; 9 | } 10 | 11 | .bridge .node-hexagon, .ovsbridge .node-hexagon, .port .node-hexagon, .ovsport .node-hexagon { 12 | fill: #36b791; 13 | background-color: #36b791; 14 | } 15 | 16 | .interface .node-hexagon, .device .node-hexagon, .tun .node-hexagon, .tuntap .node-hexagon, .tap .node-hexagon, .veth .node-hexagon { 17 | fill: #88D498; 18 | background-color: #88D498; 19 | } 20 | 21 | .down .node-hexagon { 22 | fill: rgb(163, 0, 0); 23 | } 24 | 25 | .host .node-icon { 26 | fill: #eee; 27 | } 28 | 29 | .netns .node-icon { 30 | fill: #444; 31 | } 32 | 33 | .bridge .node-icon, .ovsbridge .node-icon, .port .node-icon, .ovsport .node-icon { 34 | fill: #eee; 35 | } 36 | 37 | .interface .node-icon, .device .node-icon, .tun .node-icon, .tap .node-icon, .veth .node-icon { 38 | fill: #444; 39 | } 40 | 41 | .down .node-icon { 42 | fill: #eee; 43 | } -------------------------------------------------------------------------------- /entry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -f /dump.json ]; then 4 | /usr/bin/skydive --conf /etc/skydive.yml analyzer & 5 | sleep 15 6 | 7 | /usr/bin/skydive client topology import --file /dump.json 8 | fi 9 | 10 | echo "########## Skydive WebUI - HTTP Server started ##########" 11 | python3 -m http.server 8080 --directory dist/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@skydive-project/skydive-ui", 3 | "version": "0.1.8", 4 | "main": "y", 5 | "dependencies": { 6 | "redux": "^4.0.5" 7 | }, 8 | "devDependencies": { 9 | "@babel/preset-env": "^7.12.7", 10 | "@babel/preset-react": "^7.12.7", 11 | "@date-io/date-fns": "^1.3.13", 12 | "@fortawesome/fontawesome-free": "^5.15.1", 13 | "@material-ui/core": "^4.11.0", 14 | "@material-ui/icons": "^4.9.1", 15 | "@material-ui/lab": "^4.0.0-alpha.57", 16 | "@material-ui/pickers": "^3.2.10", 17 | "@types/chai": "^4.2.14", 18 | "@types/mocha": "^5.2.7", 19 | "@types/react": "^16.14.1", 20 | "@types/react-dom": "^16.9.10", 21 | "@types/styled-jsx": "^2.2.8", 22 | "awesome-typescript-loader": "^5.2.1", 23 | "chai": "^4.2.0", 24 | "clsx": "^1.1.1", 25 | "copy-webpack-plugin": "^6.3.2", 26 | "css-loader": "^3.6.0", 27 | "d3-flextree": "^2.1.1", 28 | "d3-hierarchy": "^1.1.9", 29 | "d3-polygon": "^1.0.6", 30 | "d3-selection": "^1.4.2", 31 | "d3-shape": "^1.3.7", 32 | "d3-transition": "^1.3.2", 33 | "d3-zoom": "^1.7.3", 34 | "date-fns": "^2.16.1", 35 | "downshift": "^3.4.8", 36 | "es6-promise": "^4.2.8", 37 | "fetch-jsonp": "^1.1.3", 38 | "file-loader": "^6.2.0", 39 | "history": "^4.10.1", 40 | "html-webpack-plugin": "^5.3.2", 41 | "lodash.deburr": "^4.1.0", 42 | "mocha": "^8.2.1", 43 | "moment": "^2.29.1", 44 | "mui-datatables": "^3.7.3", 45 | "node-fetch": ">=2.6.1", 46 | "notistack": "^1.0.2", 47 | "nyc": "^14.1.1", 48 | "portable-fetch": "^3.0.0", 49 | "query-string": "^6.13.7", 50 | "react": "^16.14.0", 51 | "react-docgen": "^4.1.1", 52 | "react-dom": "^16.14.0", 53 | "react-google-charts": "^3.0.15", 54 | "react-json-tree": "^0.11.2", 55 | "react-redux": "^7.2.2", 56 | "react-resize-observer": "^1.1.1", 57 | "react-router-dom": "^5.2.0", 58 | "react-sliding-pane": "^3.1.0", 59 | "react-syntax-highlighter": "^15.4.3", 60 | "react-websocket": "^2.1.0", 61 | "roboto-fontface": "^0.10.0", 62 | "source-map-loader": "^0.2.4", 63 | "style-loader": "^1.3.0", 64 | "throttle-debounce": "^2.3.0", 65 | "ts-node": "^8.10.2", 66 | "typescript": "^3.9.7", 67 | "url-loader": "^4.1.1", 68 | "webpack": "^5.21.0", 69 | "webpack-cli": "^4.2.0", 70 | "webpack-dev-server": "^3.11.0" 71 | }, 72 | "scripts": { 73 | "start": "webpack serve --mode development --disable-host-check --host 0.0.0.0", 74 | "build": "webpack", 75 | "test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha -r ts-node/register tests/**/*.test.ts", 76 | "coverage": "nyc -r lcov -e .ts -x \"*.test.ts\" npm run test" 77 | }, 78 | "repository": { 79 | "type": "git", 80 | "url": "git+https://github.com/skydive-project/skydive-ui.git" 81 | }, 82 | "keywords": [ 83 | "Skydive", 84 | "WebUI", 85 | "v2" 86 | ], 87 | "author": "Sylvain Afchain", 88 | "license": "Apache-2.0", 89 | "bugs": { 90 | "url": "https://github.com/skydive-project/skydive-ui/issues" 91 | }, 92 | "homepage": "https://github.com/skydive-project/skydive-ui#readme", 93 | "description": "Skydive WebUI v2", 94 | "directories": { 95 | "test": "tests" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /samples/datacenter.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Edges": [ 4 | { 5 | "Child": "host-0", 6 | "ID": "Edge-host-host-0-data_center-data_center-0-ownership", 7 | "Metadata": { 8 | "RelationType": "ownership" 9 | }, 10 | "Origin": "SoftLayer", 11 | "Parent": "data_center-0" 12 | }, 13 | { 14 | "Child": "host-1", 15 | "ID": "Edge-host-host-1-data_center-data_center-0-ownership", 16 | "Metadata": { 17 | "RelationType": "ownership" 18 | }, 19 | "Origin": "SoftLayer", 20 | "Parent": "data_center-0" 21 | }, 22 | { 23 | "Child": "host-2", 24 | "ID": "Edge-host-host-2-data_center-data_center-1-ownership", 25 | "Metadata": { 26 | "RelationType": "ownership" 27 | }, 28 | "Origin": "SoftLayer", 29 | "Parent": "data_center-1" 30 | }, 31 | { 32 | "Child": "host-3", 33 | "ID": "Edge-host-host-3-data_center-data_center-1-ownership", 34 | "Metadata": { 35 | "RelationType": "ownership" 36 | }, 37 | "Origin": "SoftLayer", 38 | "Parent": "data_center-1" 39 | }, 40 | { 41 | "Child": "vm-0", 42 | "ID": "Edge-vm-vm-0-host-host-0-ownership", 43 | "Metadata": { 44 | "RelationType": "ownership" 45 | }, 46 | "Origin": "SoftLayer", 47 | "Parent": "host-0" 48 | }, 49 | { 50 | "Child": "vm-1", 51 | "ID": "Edge-vm-vm-1-host-host-0-ownership", 52 | "Metadata": { 53 | "RelationType": "ownership" 54 | }, 55 | "Origin": "SoftLayer", 56 | "Parent": "host-0" 57 | }, 58 | { 59 | "Child": "vm-2", 60 | "ID": "Edge-vm-vm-2-host-host-1-ownership", 61 | "Metadata": { 62 | "RelationType": "ownership" 63 | }, 64 | "Origin": "SoftLayer", 65 | "Parent": "host-1" 66 | }, 67 | { 68 | "Child": "vm-3", 69 | "ID": "Edge-vm-vm-3-host-host-1-ownership", 70 | "Metadata": { 71 | "RelationType": "ownership" 72 | }, 73 | "Origin": "SoftLayer", 74 | "Parent": "host-1" 75 | }, 76 | { 77 | "Child": "vm-4", 78 | "ID": "Edge-vm-vm-4-host-host-2-ownership", 79 | "Metadata": { 80 | "RelationType": "ownership" 81 | }, 82 | "Origin": "SoftLayer", 83 | "Parent": "host-2" 84 | }, 85 | { 86 | "Child": "vm-5", 87 | "ID": "Edge-vm-vm-5-host-host-3-ownership", 88 | "Metadata": { 89 | "RelationType": "ownership" 90 | }, 91 | "Origin": "SoftLayer", 92 | "Parent": "host-3" 93 | }, 94 | { 95 | "Child": "vm-6", 96 | "ID": "Edge-vm-vm-6-host-host-3-ownership", 97 | "Metadata": { 98 | "RelationType": "ownership" 99 | }, 100 | "Origin": "SoftLayer", 101 | "Parent": "host-3" 102 | }, 103 | { 104 | "Child": "vm-0", 105 | "ID": "Edge-vm-vm-0-application-application-1-application", 106 | "Metadata": { 107 | "RelationType": "application" 108 | }, 109 | "Origin": "SoftLayer", 110 | "Parent": "application-1" 111 | }, 112 | { 113 | "Child": "vm-1", 114 | "ID": "Edge-vm-vm-1-application-application-0-application", 115 | "Metadata": { 116 | "RelationType": "application" 117 | }, 118 | "Origin": "SoftLayer", 119 | "Parent": "application-0" 120 | }, 121 | { 122 | "Child": "vm-2", 123 | "ID": "Edge-vm-vm-2-application-application-0-application", 124 | "Metadata": { 125 | "RelationType": "application" 126 | }, 127 | "Origin": "SoftLayer", 128 | "Parent": "application-0" 129 | }, 130 | { 131 | "Child": "vm-3", 132 | "ID": "Edge-vm-vm-3-application-application-0-application", 133 | "Metadata": { 134 | "RelationType": "application" 135 | }, 136 | "Origin": "SoftLayer", 137 | "Parent": "application-0" 138 | }, 139 | { 140 | "Child": "vm-4", 141 | "ID": "Edge-vm-vm-4-application-application-0-application", 142 | "Metadata": { 143 | "RelationType": "application" 144 | }, 145 | "Origin": "SoftLayer", 146 | "Parent": "application-0" 147 | }, 148 | { 149 | "Child": "vm-5", 150 | "ID": "Edge-vm-vm-5-application-application-1-application", 151 | "Metadata": { 152 | "RelationType": "application" 153 | }, 154 | "Origin": "SoftLayer", 155 | "Parent": "application-1" 156 | }, 157 | { 158 | "Child": "vm-6", 159 | "ID": "Edge-vm-vm-6-application-application-0-application", 160 | "Metadata": { 161 | "RelationType": "application" 162 | }, 163 | "Origin": "SoftLayer", 164 | "Parent": "application-0" 165 | } 166 | ], 167 | "Nodes": [ 168 | { 169 | "ID": "application-0", 170 | "Metadata": { 171 | "Name": "Database", 172 | "Type": "application" 173 | }, 174 | "Origin": "SoftLayer" 175 | }, 176 | { 177 | "ID": "application-1", 178 | "Metadata": { 179 | "Name": "Web", 180 | "Type": "application" 181 | }, 182 | "Origin": "SoftLayer" 183 | }, 184 | { 185 | "ID": "host-0", 186 | "Metadata": { 187 | "CPUs": "4", 188 | "DataCenter": "DataCenter1", 189 | "Memory": "64", 190 | "Name": "Host1", 191 | "Type": "host" 192 | }, 193 | "Origin": "SoftLayer" 194 | }, 195 | { 196 | "ID": "host-1", 197 | "Metadata": { 198 | "CPUs": "4", 199 | "DataCenter": "DataCenter1", 200 | "Memory": "64", 201 | "Name": "Host2", 202 | "Type": "host" 203 | }, 204 | "Origin": "SoftLayer" 205 | }, 206 | { 207 | "ID": "host-2", 208 | "Metadata": { 209 | "CPUs": "4", 210 | "DataCenter": "DataCenter2", 211 | "Memory": "128", 212 | "Name": "Host3", 213 | "Type": "host" 214 | }, 215 | "Origin": "SoftLayer" 216 | }, 217 | { 218 | "ID": "host-3", 219 | "Metadata": { 220 | "CPUs": "16", 221 | "DataCenter": "DataCenter2", 222 | "Memory": "64", 223 | "Name": "Host4", 224 | "Type": "host" 225 | }, 226 | "Origin": "SoftLayer" 227 | }, 228 | { 229 | "ID": "vm-0", 230 | "Metadata": { 231 | "Application": "Web", 232 | "Host": "Host1", 233 | "Name": "VM1", 234 | "Type": "vm" 235 | }, 236 | "Origin": "SoftLayer" 237 | }, 238 | { 239 | "ID": "vm-1", 240 | "Metadata": { 241 | "Application": "Database", 242 | "Host": "Host1", 243 | "Name": "VM2", 244 | "Type": "vm" 245 | }, 246 | "Origin": "SoftLayer" 247 | }, 248 | { 249 | "ID": "vm-2", 250 | "Metadata": { 251 | "Application": "Database", 252 | "Host": "Host2", 253 | "Name": "VM3", 254 | "Type": "vm" 255 | }, 256 | "Origin": "SoftLayer" 257 | }, 258 | { 259 | "ID": "vm-3", 260 | "Metadata": { 261 | "Application": "Database", 262 | "Host": "Host2", 263 | "Name": "VM4", 264 | "Type": "vm" 265 | }, 266 | "Origin": "SoftLayer" 267 | }, 268 | { 269 | "ID": "vm-4", 270 | "Metadata": { 271 | "Application": "Database", 272 | "Host": "Host3", 273 | "Name": "VM5", 274 | "Type": "vm" 275 | }, 276 | "Origin": "SoftLayer" 277 | }, 278 | { 279 | "ID": "vm-5", 280 | "Metadata": { 281 | "Application": "Web", 282 | "Host": "Host4", 283 | "Name": "VM6", 284 | "Type": "vm" 285 | }, 286 | "Origin": "SoftLayer" 287 | }, 288 | { 289 | "ID": "vm-6", 290 | "Metadata": { 291 | "Application": "Database", 292 | "Host": "Host4", 293 | "Name": "VM7", 294 | "Type": "vm" 295 | }, 296 | "Origin": "SoftLayer" 297 | }, 298 | { 299 | "ID": "data_center-0", 300 | "Metadata": { 301 | "Location": "East", 302 | "Name": "DataCenter1", 303 | "Type": "data_center" 304 | }, 305 | "Origin": "SoftLayer" 306 | }, 307 | { 308 | "ID": "data_center-1", 309 | "Metadata": { 310 | "Location": "West", 311 | "Name": "DataCenter2", 312 | "Type": "data_center" 313 | }, 314 | "Origin": "SoftLayer" 315 | } 316 | ] 317 | } 318 | ] 319 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/screenshot.png -------------------------------------------------------------------------------- /src/About.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from 'react' 19 | import Button from '@material-ui/core/Button' 20 | import Dialog from '@material-ui/core/Dialog' 21 | import DialogActions from '@material-ui/core/DialogActions' 22 | import DialogContent from '@material-ui/core/DialogContent' 23 | import DialogContentText from '@material-ui/core/DialogContentText' 24 | import DialogTitle from '@material-ui/core/DialogTitle' 25 | import { Typography } from '@material-ui/core' 26 | 27 | export interface AboutProps { 28 | open: boolean 29 | onClose: () => void 30 | appName: string 31 | appVersion: string 32 | uiVersion: string 33 | } 34 | 35 | export default function AboutDialog(props: AboutProps) { 36 | const { open, onClose, appName, appVersion, uiVersion, ...other } = props 37 | 38 | return ( 39 | 43 | {appName} 44 | 45 |
46 | {appVersion && 47 | 48 | {appName} version : {appVersion} 49 | 50 | } 51 | 52 | UI version : {uiVersion} 53 | 54 |
55 |
56 | 57 | 60 | 61 |
62 | ) 63 | } -------------------------------------------------------------------------------- /src/ActionButtons/Capture.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from 'react' 19 | import { withStyles } from '@material-ui/core/styles' 20 | import Tooltip from '@material-ui/core/Tooltip' 21 | import IconButton from '@material-ui/core/IconButton' 22 | import VideocamIcon from '@material-ui/icons/Videocam' 23 | 24 | import { styles } from '../DataPanels/PanelStyles' 25 | import { Node, Link } from '../Topology' 26 | 27 | interface Props { 28 | el: Node | Link 29 | onClick: (el: Node | Link) => void 30 | } 31 | 32 | export class CapturePanel extends React.Component { 33 | 34 | constructor(props) { 35 | super(props) 36 | } 37 | 38 | render() { 39 | return ( 40 | 41 | { 42 | this.props.el.type === 'node' && 43 | 44 | this.props.onClick(this.props.el)} 47 | color="inherit"> 48 | 49 | 50 | 51 | } 52 | 53 | ) 54 | } 55 | } 56 | 57 | export default withStyles(styles)(CapturePanel) -------------------------------------------------------------------------------- /src/ActionButtons/Gremlin.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from 'react' 19 | import { withStyles } from '@material-ui/core/styles' 20 | import Tooltip from '@material-ui/core/Tooltip' 21 | import IconButton from '@material-ui/core/IconButton' 22 | import CodeIcon from '@material-ui/icons/Code' 23 | 24 | import { styles } from '../DataPanels/PanelStyles' 25 | import { Node, Link } from '../Topology' 26 | 27 | interface Props { 28 | el: Node | Link 29 | onClick: (el: Node | Link) => void 30 | } 31 | 32 | export class GremlinButton extends React.Component { 33 | 34 | constructor(props) { 35 | super(props) 36 | } 37 | 38 | render() { 39 | return ( 40 | 41 | this.props.onClick(this.props.el)} 44 | color="inherit"> 45 | 46 | 47 | 48 | ) 49 | } 50 | } 51 | 52 | export default withStyles(styles)(GremlinButton) -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .header { 2 | position: absolute; 3 | padding: 10px; 4 | background-color: #000000c7; 5 | width: 100%; 6 | text-align: left; 7 | } 8 | 9 | .logo img { 10 | width:120px; 11 | height:auto; 12 | } 13 | 14 | .header i { 15 | color: #eee; 16 | font-size: 28px; 17 | font-weight: normal; 18 | margin-right: 10px; 19 | vertical-align: text-bottom; 20 | } 21 | 22 | .MuiIconButton-root:hover { 23 | background-color: #555 !important; 24 | } 25 | 26 | .MuiListItemIcon-root { 27 | min-width: 32px !important; 28 | } -------------------------------------------------------------------------------- /src/AppStyles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import { createStyles, Theme } from '@material-ui/core' 19 | import { fade } from '@material-ui/core/styles' 20 | 21 | const drawerWidth = 300 22 | 23 | export const styles = (theme: Theme) => createStyles({ 24 | app: { 25 | display: 'flex', 26 | }, 27 | grow: { 28 | flexGrow: 1 29 | }, 30 | avatar: { 31 | margin: 10, 32 | color: '#121212', 33 | backgroundColor: '#757575' 34 | }, 35 | toolbar: { 36 | paddingRight: 24, // keep right padding when drawer closed 37 | }, 38 | toolbarIcon: { 39 | display: 'flex', 40 | alignItems: 'center', 41 | justifyContent: 'flex-end', 42 | padding: '0 8px', 43 | ...theme.mixins.toolbar, 44 | }, 45 | appBar: { 46 | backgroundColor: theme.palette.common.black, 47 | zIndex: theme.zIndex.drawer + 1, 48 | transition: theme.transitions.create(['width', 'margin'], { 49 | easing: theme.transitions.easing.sharp, 50 | duration: theme.transitions.duration.leavingScreen, 51 | }), 52 | }, 53 | appBarShift: { 54 | marginLeft: drawerWidth, 55 | width: `calc(100% - ${drawerWidth}px)`, 56 | transition: theme.transitions.create(['width', 'margin'], { 57 | easing: theme.transitions.easing.sharp, 58 | duration: theme.transitions.duration.enteringScreen, 59 | }), 60 | }, 61 | menuButton: { 62 | marginRight: 20, 63 | }, 64 | menuButtonHidden: { 65 | display: 'none', 66 | }, 67 | title: { 68 | display: 'none', 69 | [theme.breakpoints.up('sm')]: { 70 | display: 'block', 71 | }, 72 | }, 73 | subTitle: { 74 | fontStyle: 'italic', 75 | color: '#ddd' 76 | }, 77 | drawerPaper: { 78 | position: 'relative', 79 | whiteSpace: 'nowrap', 80 | width: drawerWidth, 81 | transition: theme.transitions.create('width', { 82 | easing: theme.transitions.easing.sharp, 83 | duration: theme.transitions.duration.enteringScreen, 84 | }), 85 | }, 86 | drawerPaperClose: { 87 | overflowX: 'hidden', 88 | transition: theme.transitions.create('width', { 89 | easing: theme.transitions.easing.sharp, 90 | duration: theme.transitions.duration.leavingScreen, 91 | }), 92 | width: theme.spacing(7), 93 | [theme.breakpoints.up('sm')]: { 94 | width: theme.spacing(0), 95 | }, 96 | }, 97 | content: { 98 | flexGrow: 1, 99 | height: '100vh', 100 | overflow: 'auto', 101 | }, 102 | container: { 103 | paddingTop: theme.spacing(0), 104 | paddingBottom: theme.spacing(0), 105 | paddingLeft: theme.spacing(0), 106 | paddingRight: theme.spacing(0), 107 | }, 108 | topology: { 109 | height: `calc(100vh - 10px)`, 110 | }, 111 | rightPanel: { 112 | position: 'absolute', 113 | top: 65, 114 | right: 0, 115 | bottom: 0, 116 | maxWidth: 'unset', 117 | width: 'unset', 118 | zIndex: 1000, 119 | paddingTop: theme.spacing(0), 120 | paddingBottom: theme.spacing(0), 121 | paddingLeft: theme.spacing(0), 122 | paddingRight: theme.spacing(0), 123 | }, 124 | rightPanelPaper: { 125 | overflow: 'hidden', 126 | position: 'relative', 127 | display: 'flex', 128 | flexDirection: 'column', 129 | width: 600, 130 | [theme.breakpoints.down('xl')]: { 131 | width: 500 132 | }, 133 | height: `100%`, 134 | transition: theme.transitions.create('width', { 135 | easing: theme.transitions.easing.sharp, 136 | duration: theme.transitions.duration.enteringScreen, 137 | }) 138 | }, 139 | rightPanelPaperClose: { 140 | overflow: 'hidden', 141 | transition: theme.transitions.create('width', { 142 | easing: theme.transitions.easing.sharp, 143 | duration: theme.transitions.duration.leavingScreen, 144 | }), 145 | width: theme.spacing(7), 146 | [theme.breakpoints.up('sm')]: { 147 | width: theme.spacing(0), 148 | }, 149 | }, 150 | nodeTagsPanel: { 151 | position: 'absolute', 152 | left: 15, 153 | top: 80, 154 | maxWidth: 'unset', 155 | width: 'unset', 156 | paddingTop: theme.spacing(0), 157 | paddingBottom: theme.spacing(0), 158 | paddingLeft: theme.spacing(0), 159 | paddingRight: theme.spacing(0), 160 | }, 161 | nodeTagsFab: { 162 | margin: theme.spacing(1), 163 | boxShadow: 'unset', 164 | fontWeight: 'unset', 165 | fontSize: '0.8rem', 166 | padding: '0 12px !important' 167 | }, 168 | linkTagsPanel: { 169 | position: 'absolute', 170 | left: 20, 171 | bottom: 20, 172 | maxWidth: 'unset', 173 | width: 'unset', 174 | paddingTop: theme.spacing(0), 175 | paddingBottom: theme.spacing(0), 176 | paddingLeft: theme.spacing(0), 177 | paddingRight: theme.spacing(0), 178 | }, 179 | linkTagsPanelPaper: { 180 | position: 'relative', 181 | display: 'flex', 182 | flexDirection: 'column', 183 | minWidth: '100px', 184 | padding: theme.spacing(2), 185 | border: '1px solid #ddd' 186 | }, 187 | search: { 188 | padding: theme.spacing(0.2), 189 | position: 'relative', 190 | borderRadius: theme.shape.borderRadius, 191 | backgroundColor: fade(theme.palette.common.white, 0.15), 192 | '&:hover': { 193 | backgroundColor: fade(theme.palette.common.white, 0.25), 194 | }, 195 | marginRight: theme.spacing(2), 196 | marginLeft: 0, 197 | width: '100%', 198 | [theme.breakpoints.up('sm')]: { 199 | marginLeft: theme.spacing(3), 200 | width: 'auto', 201 | }, 202 | lineHeight: 1, 203 | fontSize: '2rem' 204 | }, 205 | filtersPanel: { 206 | position: 'absolute', 207 | right: 20, 208 | top: 80, 209 | maxWidth: 'unset', 210 | width: 'unset', 211 | paddingTop: theme.spacing(0), 212 | paddingBottom: theme.spacing(0), 213 | paddingLeft: theme.spacing(0), 214 | paddingRight: theme.spacing(0), 215 | backgroundColor: theme.palette.common.white, 216 | }, 217 | filtersFab: { 218 | margin: theme.spacing(1), 219 | boxShadow: 'unset', 220 | fontWeight: 'unset', 221 | fontSize: '0.8rem', 222 | padding: '6px 12px !important' 223 | }, 224 | menuItemIconFree: { 225 | fontFamily: `"Font Awesome 5 Free" !important`, 226 | fontWeight: 900, 227 | fontSize: 16, 228 | marginBottom: `0 !important`, 229 | minWidth: 32 230 | }, 231 | menuItemIconBrands: { 232 | fontFamily: `"Font Awesome 5 Brands" !important`, 233 | fontWeight: 900, 234 | fontSize: 16, 235 | marginBottom: `0 !important`, 236 | minWidth: 32 237 | }, 238 | menuItemIconImg: { 239 | maxWidth: 18, 240 | maxHeight: 18, 241 | verticalAlign: 'middle' 242 | } 243 | }) -------------------------------------------------------------------------------- /src/AutoComplete.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from 'react' 19 | import deburr from 'lodash/deburr' 20 | import Downshift from 'downshift' 21 | import TextField, { StandardTextFieldProps } from '@material-ui/core/TextField' 22 | import Paper from '@material-ui/core/Paper' 23 | import MenuItem, { MenuItemProps } from '@material-ui/core/MenuItem' 24 | import Chip from '@material-ui/core/Chip' 25 | import InputAdornment from '@material-ui/core/InputAdornment' 26 | import SearchIcon from '@material-ui/icons/Search' 27 | 28 | import { styles } from './AutoCompleteStyles' 29 | 30 | type RenderInputProps = StandardTextFieldProps & { 31 | classes: ReturnType 32 | ref?: React.Ref 33 | } 34 | 35 | function renderInput(inputProps: RenderInputProps) { 36 | const { InputProps, classes, ref, ...other } = inputProps 37 | 38 | return ( 39 | 51 | ) 52 | } 53 | 54 | interface RenderSuggestionProps { 55 | highlightedIndex: number | null 56 | index: number 57 | itemProps: MenuItemProps<'div', { button?: never }> 58 | selectedItem: string 59 | suggestion: string 60 | } 61 | 62 | function renderSuggestion(suggestionProps: RenderSuggestionProps) { 63 | const { suggestion, index, itemProps, highlightedIndex, selectedItem } = suggestionProps 64 | const isHighlighted = highlightedIndex === index 65 | const isSelected = (selectedItem || '').indexOf(suggestion) > -1 66 | 67 | return ( 68 | 77 | {suggestion} 78 | 79 | ) 80 | } 81 | 82 | function getSuggestions(suggestions: Array, value: string, { showEmpty = false } = {}) { 83 | const inputValue = deburr(value.trim()).toLowerCase() 84 | const inputLength = inputValue.length 85 | let count = 0 86 | 87 | return inputLength === 0 && !showEmpty 88 | ? [] 89 | : suggestions.filter(suggestion => { 90 | const keep = 91 | count < 5 && suggestion.slice(0, inputLength).toLowerCase() === inputValue 92 | 93 | if (keep) { 94 | count += 1 95 | } 96 | 97 | return keep 98 | }) 99 | } 100 | 101 | interface AutocompleteProps { 102 | placeholder: string 103 | suggestions: Array 104 | onChange: (selected: Array) => void 105 | } 106 | 107 | export default function Autocomplete(props: AutocompleteProps) { 108 | const { placeholder, suggestions, onChange } = props 109 | 110 | const classes = styles({}) 111 | const [inputValue, setInputValue] = React.useState('') 112 | const [selectedItem, setSelectedItem] = React.useState>([]) 113 | 114 | function handleKeyDown(event: React.KeyboardEvent) { 115 | if (selectedItem.length && !inputValue.length && event.key === 'Backspace') { 116 | setSelectedItem(selectedItem.slice(0, selectedItem.length - 1)) 117 | } 118 | } 119 | 120 | function handleInputChange(event: React.ChangeEvent<{ value: string }>) { 121 | setInputValue(event.target.value) 122 | } 123 | 124 | function handleChange(item: string) { 125 | let newSelectedItem = [...selectedItem] 126 | if (item && newSelectedItem.indexOf(item) === -1) { 127 | newSelectedItem = [...newSelectedItem, item] 128 | } 129 | setInputValue('') 130 | setSelectedItem(newSelectedItem) 131 | 132 | onChange(newSelectedItem) 133 | } 134 | 135 | const handleDelete = (item: string) => () => { 136 | const newSelectedItem = [...selectedItem] 137 | newSelectedItem.splice(newSelectedItem.indexOf(item), 1) 138 | setSelectedItem(newSelectedItem) 139 | 140 | onChange(newSelectedItem) 141 | } 142 | 143 | return ( 144 | 149 | {({ 150 | getInputProps, 151 | getItemProps, 152 | getLabelProps, 153 | isOpen, 154 | inputValue: inputValue2, 155 | selectedItem: selectedItem2, 156 | highlightedIndex, 157 | }) => { 158 | const { onChange, ...inputProps } = getInputProps({ 159 | onKeyDown: handleKeyDown, 160 | placeholder: placeholder, 161 | }) 162 | return ( 163 |
164 | {renderInput({ 165 | fullWidth: true, 166 | classes, 167 | InputLabelProps: getLabelProps(), 168 | InputProps: { 169 | disableUnderline: true, 170 | startAdornment: ( 171 | 172 | 173 | { 174 | selectedItem.map(item => ( 175 | )) 182 | } 183 | 184 | ), 185 | onChange: event => { 186 | handleInputChange(event) 187 | onChange!(event as React.ChangeEvent) 188 | } 189 | }, 190 | inputProps, 191 | })} 192 | {isOpen ? ( 193 | 194 | {getSuggestions(suggestions, inputValue2!).map((suggestion, index) => 195 | renderSuggestion({ 196 | suggestion, 197 | index, 198 | itemProps: getItemProps({ item: suggestion }), 199 | highlightedIndex, 200 | selectedItem: selectedItem2, 201 | }), 202 | )} 203 | 204 | ) : null} 205 |
206 | ) 207 | }} 208 |
209 | ) 210 | } -------------------------------------------------------------------------------- /src/AutoCompleteStyles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import { createStyles, makeStyles, Theme } from '@material-ui/core' 19 | 20 | export const styles = makeStyles((theme: Theme) => 21 | createStyles({ 22 | root: { 23 | flexGrow: 1, 24 | height: 250, 25 | }, 26 | container: { 27 | flexGrow: 1, 28 | position: 'relative', 29 | }, 30 | paper: { 31 | position: 'absolute', 32 | zIndex: 1, 33 | marginTop: theme.spacing(1), 34 | left: 0, 35 | right: 0, 36 | }, 37 | chip: { 38 | margin: theme.spacing(0.5, 0.25), 39 | }, 40 | inputRoot: { 41 | flexWrap: 'wrap', 42 | color: 'inherit', 43 | }, 44 | inputInput: { 45 | transition: theme.transitions.create('width'), 46 | width: '100%', 47 | [theme.breakpoints.up('md')]: { 48 | width: 200, 49 | }, 50 | } 51 | }) 52 | ) -------------------------------------------------------------------------------- /src/DataPanels/Capture.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from 'react' 19 | import { withStyles } from '@material-ui/core/styles' 20 | import Collapse from '@material-ui/core/Collapse' 21 | 22 | 23 | import CaptureForm from "./CaptureForm" 24 | import { styles } from './CaptureStyles' 25 | import { Node, Link } from '../Topology' 26 | import ConfigReducer from '../Config' 27 | 28 | interface Props { 29 | classes: any 30 | el: Node | Link 31 | expanded: boolean 32 | config: ConfigReducer 33 | } 34 | 35 | export class CapturePanel extends React.Component { 36 | 37 | constructor(props: Props) { 38 | super(props) 39 | } 40 | 41 | private dataAttrs(el: Node | Link): any { 42 | if (el.type === 'node') { 43 | return this.props.config.nodeAttrs(el) 44 | } else { 45 | return this.props.config.linkAttrs(el) 46 | } 47 | } 48 | 49 | render() { 50 | var classes = this.props.classes 51 | 52 | return ( 53 | 54 | 55 | 56 | ) 57 | } 58 | } 59 | 60 | export default withStyles(styles)(CapturePanel) -------------------------------------------------------------------------------- /src/DataPanels/CaptureForm.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from 'react' 19 | import TextField from '@material-ui/core/TextField' 20 | import Button from '@material-ui/core/Button' 21 | import { withStyles } from '@material-ui/core/styles' 22 | import VideocamIcon from '@material-ui/icons/Videocam' 23 | import Accordion from '@material-ui/core/Accordion' 24 | import AccordionSummary from '@material-ui/core/AccordionSummary' 25 | import AccordionDetails from '@material-ui/core/AccordionDetails' 26 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore' 27 | import InputLabel from '@material-ui/core/InputLabel' 28 | import Select from '@material-ui/core/Select' 29 | import MenuItem from '@material-ui/core/MenuItem' 30 | import FormControl from '@material-ui/core/FormControl' 31 | import Typography from '@material-ui/core/Typography' 32 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 33 | import Checkbox from '@material-ui/core/Checkbox' 34 | 35 | import { Configuration } from '../api/configuration' 36 | import Panel from './Panel' 37 | import { CapturesApi } from '../api' 38 | import { styles } from './CaptureFormStyles' 39 | import { AppState, session } from '../Store' 40 | import { connect } from 'react-redux' 41 | 42 | interface Props { 43 | classes: any 44 | defaultName: string 45 | gremlin: string 46 | session: session 47 | } 48 | 49 | interface State { 50 | name: string 51 | description: string 52 | bpf: string 53 | } 54 | 55 | class CaptureForm extends React.Component { 56 | 57 | state: State 58 | 59 | constructor(props) { 60 | super(props) 61 | 62 | this.state = { 63 | name: "", 64 | description: "", 65 | bpf: "" 66 | } 67 | } 68 | 69 | onClick() { 70 | var conf = new Configuration({ basePath: this.props.session.endpoint + "/api", accessToken: this.props.session.token }) 71 | var api = new CapturesApi(conf) 72 | 73 | api.createCapture({ GremlinQuery: this.props.gremlin }).then(result => { 74 | console.log(result) 75 | }) 76 | } 77 | 78 | render() { 79 | const { classes } = this.props 80 | 81 | return ( 82 | } title="Packet capture" content={ 83 | 84 | 91 | 98 | 104 | 105 | } 107 | aria-controls="panel1a-content" 108 | id="panel1a-header" 109 | className={classes.advancedSummary} 110 | > 111 | Advanced options 112 | 113 | 114 | 115 | Capture Type 117 | 130 | 131 | 132 | Layers used for Flow Key 134 | 142 | 143 | 149 | 150 | 156 | } 157 | label="Extra TCP metric" 158 | /> 159 | 165 | } 166 | label="Defragment IPv4 packets" 167 | /> 168 | 174 | } 175 | label="Reassemble TCP packets" 176 | /> 177 | 178 | 185 | 186 | 187 | 190 | 191 | } /> 192 | ) 193 | } 194 | } 195 | 196 | export const mapStateToProps = (state: AppState) => ({ 197 | session: state.session 198 | }) 199 | 200 | export const mapDispatchToProps = ({ 201 | }) 202 | 203 | export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(CaptureForm)) -------------------------------------------------------------------------------- /src/DataPanels/CaptureFormStyles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import { createStyles, Theme } from '@material-ui/core/styles' 19 | 20 | export const styles = (theme: Theme) => createStyles({ 21 | textField: { 22 | marginLeft: theme.spacing(1), 23 | marginRight: theme.spacing(1) 24 | }, 25 | button: { 26 | margin: theme.spacing(1), 27 | }, 28 | control: { 29 | display: "block !important", 30 | "& .MuiInputBase-root": { 31 | display: "block !important" 32 | }, 33 | marginTop: 36, 34 | marginLeft: theme.spacing(1), 35 | marginRight: theme.spacing(1) 36 | }, 37 | advanced: { 38 | boxShadow: "unset !important", 39 | '&::before': { 40 | top: 0, 41 | height: 0 42 | } 43 | }, 44 | advancedSummary: { 45 | padding: 0, 46 | marginLeft: 8, 47 | marginRight: 8, 48 | color: "#061775", 49 | backgroundColor: "unset !important", 50 | borderColor: "unset", 51 | '& .MuiAccordionSummary-content': { 52 | backgroundColor: "unset", 53 | } 54 | } 55 | }) -------------------------------------------------------------------------------- /src/DataPanels/CaptureStyles.ts: -------------------------------------------------------------------------------- 1 | import { minWidth, borderRadius } from "@material-ui/system"; 2 | 3 | /* 4 | * Copyright (C) 2019 Sylvain Afchain 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | */ 19 | 20 | import { createStyles, Theme } from '@material-ui/core'; 21 | 22 | export const styles = (theme: Theme) => createStyles({ 23 | panel: { 24 | marginBottom: "16px" 25 | } 26 | }) -------------------------------------------------------------------------------- /src/DataPanels/Flow.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from "react" 19 | import { connect } from 'react-redux' 20 | import { withStyles } from '@material-ui/core/styles' 21 | 22 | import DataPanel from '../StdDataPanel' 23 | import '../StdDataPanel.css' 24 | import { AppState, session } from '../Store' 25 | import { Configuration } from '../api/configuration' 26 | import { TopologyApi } from '../api' 27 | import Tools from '../Tools' 28 | import { Node, Link } from '../Topology' 29 | import { styles } from './FlowStyles' 30 | 31 | interface Props { 32 | classes: any 33 | el: Node | Link 34 | session: session 35 | } 36 | 37 | interface State { 38 | } 39 | 40 | class FlowPanel extends React.Component { 41 | 42 | state: State 43 | gremlin: string 44 | 45 | constructor(props) { 46 | super(props) 47 | 48 | this.state = {} 49 | 50 | this.gremlin = `G.V('${this.props.el.id}').Flows()` 51 | } 52 | 53 | normalizer(data: Array): any { 54 | return data.map(flow => { 55 | 56 | // TODO(safchain) add more 57 | var result = { 58 | "Application": flow.Application, 59 | "LayersPath": flow.LayersPath, 60 | 61 | "L3TrackingID": flow.L3TrackingID, 62 | "ParentUUID": flow.ParentUUID, 63 | "RawPacketsCaptured": flow.RawPacketsCaptured, 64 | "NodeTID": flow.NodeTID, 65 | 66 | "Total.ABPackets": flow.Metric.ABPAckets, 67 | "Total.BAPackets": flow.Metric.BAPAckets, 68 | "Total.ABBytes": Tools.prettyBytes(flow.Metric.ABBytes), 69 | "Total.BABytes": Tools.prettyBytes(flow.Metric.BABytes), 70 | 71 | "Start": flow.Start, 72 | "Last": flow.Last 73 | } 74 | 75 | if (flow.LastUpdateMetric) { 76 | result["Last.ABPackets"] = flow.LastUpdateMetric.ABPAckets 77 | result["Last.BAPackets"] = flow.LastUpdateMetric.BAPAckets 78 | result["Last.ABBytes"] = Tools.prettyBytes(flow.LastUpdateMetric.ABBytes) 79 | result["Last.BABytes"] = Tools.prettyBytes(flow.LastUpdateMetric.BABytes) 80 | result["Last.RTT"] = (flow.LastUpdateMetric.RTT / 1000000) + " ms" 81 | } 82 | 83 | if (flow.Link) { 84 | result["Link.ID"] = flow.Link.ID 85 | result["Link.Protocol"] = flow.Link.Protocol 86 | result["Link.A"] = flow.Link.A 87 | result["Link.B"] = flow.Link.B 88 | } 89 | 90 | if (flow.Network) { 91 | result["Network.ID"] = flow.Network.ID 92 | result["Network.Protocol"] = flow.Network.Protocol 93 | result["Network.A"] = flow.Network.A 94 | result["Networl.B"] = flow.Network.B 95 | } 96 | 97 | if (flow.Transport) { 98 | result["Transport.ID"] = flow.Transport.ID 99 | result["Transport.Protocol"] = flow.Transport.Protocol 100 | result["Transport.A"] = flow.Transport.A 101 | result["Transport.B"] = flow.Transport.B 102 | } 103 | 104 | return result 105 | }) 106 | } 107 | 108 | fetchData(): Promise { 109 | var conf = new Configuration({ basePath: this.props.session.endpoint + "/api", accessToken: this.props.session.token }) 110 | var api = new TopologyApi(conf) 111 | 112 | return api.searchTopology({ GremlinQuery: this.gremlin }) 113 | } 114 | 115 | render() { 116 | var classes = this.props.classes 117 | 118 | const defaultColumns = ["Application", "Network.A", "Network.B", "Transport.A", "Transport.B", "Total.ABBytes", "Total.BAPackets"] 119 | 120 | return ( 121 |
122 | 125 |
126 | ) 127 | } 128 | } 129 | 130 | export const mapStateToProps = (state: AppState) => ({ 131 | session: state.session 132 | }) 133 | 134 | export const mapDispatchToProps = ({ 135 | }) 136 | 137 | export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(FlowPanel)) -------------------------------------------------------------------------------- /src/DataPanels/FlowStyles.ts: -------------------------------------------------------------------------------- 1 | import { minWidth, borderRadius } from "@material-ui/system"; 2 | 3 | /* 4 | * Copyright (C) 2019 Sylvain Afchain 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | */ 19 | 20 | import { createStyles, Theme } from '@material-ui/core'; 21 | 22 | export const styles = (theme: Theme) => createStyles({ 23 | panel: { 24 | marginBottom: "16px" 25 | } 26 | }) -------------------------------------------------------------------------------- /src/DataPanels/Gremlin.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from 'react' 19 | import { withStyles } from '@material-ui/core/styles' 20 | import Collapse from '@material-ui/core/Collapse' 21 | import CodeIcon from '@material-ui/icons/Code' 22 | import SyntaxHighlighter from 'react-syntax-highlighter' 23 | 24 | import Panel from './Panel' 25 | import { styles } from './GremlinStyles' 26 | 27 | import { Node, Link } from '../Topology' 28 | 29 | interface Props { 30 | classes: any 31 | el: Node | Link 32 | expanded: boolean 33 | } 34 | 35 | export class GremlinPanel extends React.Component { 36 | 37 | constructor(props: Props) { 38 | super(props) 39 | } 40 | 41 | render() { 42 | var classes = this.props.classes 43 | 44 | var gremlin: string 45 | if (this.props.el.type === 'node') { 46 | gremlin = `G.V('${this.props.el.id}')` 47 | } else { 48 | gremlin = `G.E('${this.props.el.id}')` 49 | } 50 | 51 | return ( 52 | 53 | } title="Gremlin expression" content={ 54 |
55 | 56 | {gremlin} 57 | 58 |
59 | } /> 60 |
61 | ) 62 | } 63 | } 64 | 65 | export default withStyles(styles)(GremlinPanel) -------------------------------------------------------------------------------- /src/DataPanels/GremlinStyles.ts: -------------------------------------------------------------------------------- 1 | import { minWidth, borderRadius } from "@material-ui/system"; 2 | 3 | /* 4 | * Copyright (C) 2019 Sylvain Afchain 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | */ 19 | 20 | import { createStyles, Theme } from '@material-ui/core'; 21 | 22 | export const styles = (theme: Theme) => createStyles({ 23 | panel: { 24 | marginBottom: "16px" 25 | }, 26 | gremlinExpr: { 27 | fontSize: '16px', 28 | padding: 0 29 | } 30 | }) -------------------------------------------------------------------------------- /src/DataPanels/Panel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from 'react' 19 | import Paper from '@material-ui/core/Paper' 20 | import { withStyles } from '@material-ui/core/styles' 21 | import { Typography } from '@material-ui/core' 22 | 23 | import { styles } from './PanelStyles' 24 | 25 | interface Props { 26 | classes: any 27 | icon: any 28 | title: string 29 | content: any 30 | } 31 | 32 | interface State { 33 | } 34 | 35 | class Panel extends React.Component { 36 | 37 | state: State 38 | 39 | constructor(props) { 40 | super(props) 41 | 42 | this.state = { 43 | } 44 | } 45 | 46 | render() { 47 | const { classes } = this.props 48 | 49 | return ( 50 |
51 |
52 |
53 | 54 | {this.props.icon} 55 | 56 | 57 | {this.props.title} 58 | 59 |
60 |
61 | 62 | {this.props.content} 63 | 64 |
65 | ) 66 | } 67 | } 68 | 69 | export default withStyles(styles)(Panel) -------------------------------------------------------------------------------- /src/DataPanels/PanelStyles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import { createStyles, Theme } from '@material-ui/core/styles' 19 | 20 | export const styles = (theme: Theme) => createStyles({ 21 | header: { 22 | display: 'flex', 23 | minHeight: 64, 24 | backgroundColor: '#a2a2a2 !important', 25 | padding: '0 24px 0 24px', 26 | borderTopLeftRadius: 4, 27 | borderTopRightRadius: 4, 28 | color: 'white' 29 | }, 30 | headerContent: { 31 | alignItems: 'center', 32 | margin: '20px 0', 33 | display: 'flex', 34 | flexGrow: 1, 35 | verticalAlign: 'middle' 36 | }, 37 | icon: { 38 | paddingRight: 8, 39 | lineHeight: 0 40 | }, 41 | paper: { 42 | display: 'block', 43 | flexWrap: 'wrap', 44 | padding: theme.spacing(2), 45 | marginLeft: 1, 46 | marginRight: 1 47 | } 48 | }) -------------------------------------------------------------------------------- /src/Login.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from "react" 19 | import { connect } from 'react-redux' 20 | import Container from '@material-ui/core/Container' 21 | import Typography from '@material-ui/core/Typography' 22 | import Button from '@material-ui/core/Button' 23 | import CssBaseline from '@material-ui/core/CssBaseline' 24 | import TextField from '@material-ui/core/TextField' 25 | import { withStyles } from '@material-ui/core/styles' 26 | import FormControlLabel from '@material-ui/core/FormControlLabel' 27 | import Checkbox from '@material-ui/core/Checkbox' 28 | import { withRouter } from 'react-router-dom' 29 | 30 | import { AppState, openSession, session } from './Store' 31 | import { styles } from './LoginStyles' 32 | import { Configuration } from './api/configuration' 33 | import { LoginApi } from './api' 34 | 35 | import Logo from '../assets/Logo-large.png' 36 | 37 | interface Props { 38 | classes: any 39 | openSession: typeof openSession 40 | history: any 41 | location: any 42 | session: session 43 | } 44 | 45 | interface State { 46 | endpoint: string 47 | username: string 48 | password: string 49 | submitted: boolean 50 | failure?: string 51 | persistent: boolean 52 | } 53 | 54 | class Login extends React.Component { 55 | 56 | state: State 57 | 58 | constructor(props) { 59 | super(props) 60 | 61 | this.state = { 62 | endpoint: this.props.session.endpoint, 63 | username: "", 64 | password: "", 65 | submitted: false, 66 | persistent: false 67 | } 68 | } 69 | 70 | handleChange(e) { 71 | const { name, value } = e.target 72 | switch (name) { 73 | case "endpoint": 74 | this.setState({ endpoint: value }) 75 | break 76 | case "username": 77 | this.setState({ username: value }) 78 | break 79 | case "password": 80 | this.setState({ password: value }) 81 | break 82 | case "persistent": 83 | this.setState({ persistent: Boolean(value) }) 84 | break 85 | } 86 | } 87 | 88 | handleSubmit(e) { 89 | e.preventDefault() 90 | 91 | this.setState({ submitted: true }) 92 | 93 | var endpoint = this.state.endpoint || this.props.session.endpoint 94 | 95 | var conf = new Configuration({ basePath: endpoint }) 96 | var api = new LoginApi(conf) 97 | 98 | api.login(this.state.username, this.state.password) 99 | .then(response => { 100 | if (response) { 101 | this.setState({ failure: undefined }) 102 | return response.json() 103 | } else { 104 | this.setState({ failure: "empty response" }) 105 | } 106 | }) 107 | .catch(err => { 108 | this.setState({ failure: err.statusText ? err.statusText : err.message }) 109 | }) 110 | .then(data => { 111 | if (data) { 112 | this.props.openSession(endpoint, this.state.username, data.Token, data.Permissions, this.state.persistent) 113 | 114 | var from = "/" 115 | if (this.props.location.state && this.props.location.state.from !== "/login") { 116 | from = this.props.location.state.from 117 | } 118 | this.props.history.push(from) 119 | } 120 | }) 121 | .catch(err => { 122 | this.setState({ failure: err.statusText }) 123 | }) 124 | } 125 | 126 | render() { 127 | const { classes } = this.props 128 | 129 | return ( 130 | 131 | 132 |
133 | logo 134 | 135 | SKYDIVE 136 | 137 |
138 |
139 | {this.state.failure && 140 | 141 |
{this.state.failure}
142 |
Please check Endpoint, Username or Password
143 |
144 | } 145 |
146 | 159 | {this.state.submitted && !this.state.endpoint && 160 |
Endpoint is required
161 | } 162 | 174 | 186 | } 188 | label="Remember me" 189 | name="persistent" 190 | value={true} 191 | onChange={this.handleChange.bind(this)} 192 | /> 193 | 202 | 203 |
204 |
205 | ) 206 | } 207 | } 208 | 209 | export const mapStateToProps = (state: AppState) => ({ 210 | session: state.session 211 | }) 212 | 213 | export const mapDispatchToProps = ({ 214 | openSession 215 | }) 216 | 217 | export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(withRouter(Login))) 218 | -------------------------------------------------------------------------------- /src/LoginStyles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import { createStyles, Theme } from '@material-ui/core' 19 | 20 | export const styles = (theme: Theme) => createStyles({ 21 | '@global': { 22 | body: { 23 | backgroundColor: theme.palette.common.black 24 | } 25 | }, 26 | paper: { 27 | padding: theme.spacing(4), 28 | display: 'flex', 29 | flexDirection: 'column', 30 | alignItems: 'center', 31 | backgroundColor: theme.palette.common.white, 32 | borderRadius: 5 33 | }, 34 | form: { 35 | width: '100%', 36 | marginTop: theme.spacing(1) 37 | }, 38 | submit: { 39 | margin: theme.spacing(3, 0, 2) 40 | }, 41 | error: { 42 | color: '#bb2c2c' 43 | }, 44 | failure: { 45 | color: '#bb2c2c', 46 | fontSize: 18 47 | }, 48 | logo: { 49 | marginTop: theme.spacing(12), 50 | marginBottom: theme.spacing(2), 51 | display: 'flex', 52 | flexDirection: 'column', 53 | alignItems: 'center', 54 | }, 55 | logoImg: { 56 | width: '40%', 57 | height: 'auto' 58 | }, 59 | logoTitle: { 60 | color: theme.palette.common.white, 61 | fontStyle: 'italic', 62 | fontWeight: 400 63 | } 64 | }) -------------------------------------------------------------------------------- /src/Menu.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from 'react' 19 | import ListItem from '@material-ui/core/ListItem' 20 | import ListItemIcon from '@material-ui/core/ListItemIcon' 21 | import ListItemText from '@material-ui/core/ListItemText' 22 | import SettingsIcon from '@material-ui/icons/Settings' 23 | import InfoIcon from '@material-ui/icons/Info' 24 | import LibraryBooksIcon from '@material-ui/icons/LibraryBooks' 25 | 26 | export function MenuListItems() { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | export interface HelpListItemsProps { 40 | onClick: () => void 41 | } 42 | 43 | export function HelpListItems(props: HelpListItemsProps) { 44 | const { onClick, ...other } = props 45 | 46 | return ( 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ) 62 | } -------------------------------------------------------------------------------- /src/Png.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const value: any; 3 | export default value; 4 | } -------------------------------------------------------------------------------- /src/SelectionPanel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from "react" 19 | import Tabs from '@material-ui/core/Tabs' 20 | import Tab from '@material-ui/core/Tab' 21 | import { connect } from 'react-redux' 22 | import IconButton from '@material-ui/core/IconButton' 23 | import LocationOnIcon from '@material-ui/icons/LocationOn' 24 | import CancelIcon from '@material-ui/icons/Cancel' 25 | import Tooltip from '@material-ui/core/Tooltip' 26 | import { withStyles } from '@material-ui/core/styles' 27 | 28 | import { Node, Link } from './Topology' 29 | import DataPanel from './StdDataPanel' 30 | import { a11yProps, TabPanel } from './Tabs' 31 | import { AppState } from './Store' 32 | import { styles } from './SelectionPanelStyles' 33 | import ConfigReducer from './Config' 34 | 35 | 36 | interface Props { 37 | classes: any 38 | selection: Array 39 | revision: number 40 | onLocation?: (node: Node | Link) => void 41 | onClose?: (node: Node | Link) => void 42 | config: ConfigReducer 43 | buttonsContent?: (el: Node | Link) => JSX.Element 44 | panelsContent?: (el: Node | Link) => JSX.Element 45 | } 46 | 47 | interface State { 48 | tab: number 49 | gremlin: string 50 | captureForm: boolean 51 | } 52 | 53 | class SelectionPanel extends React.Component { 54 | 55 | state: State 56 | 57 | constructor(props) { 58 | super(props) 59 | 60 | this.state = { 61 | tab: 0, 62 | gremlin: "", 63 | captureForm: false 64 | } 65 | } 66 | 67 | static getDerivedStateFromProps(props, state) { 68 | var tab = state.tab 69 | if (tab >= props.selection.length) { 70 | tab = props.selection.length - 1 71 | } 72 | if (tab < 0) { 73 | tab = 0 74 | } 75 | return { 76 | tab: tab 77 | } 78 | } 79 | 80 | private renderTabs(classes: any) { 81 | return this.props.selection.map((el: Node | Link, i: number) => { 82 | var className = classes.tabIconFree 83 | 84 | if (el.type === 'node') { 85 | let attrs = this.props.config.nodeAttrs(el) 86 | var icon: string = attrs.icon 87 | var href: string = attrs.href 88 | 89 | if (attrs.iconClass === "font-brands") { 90 | className = classes.tabIconBrands 91 | } 92 | 93 | var title = this.props.config.nodeTabTitle(el) 94 | } else { 95 | let attrs = this.props.config.linkAttrs(el) 96 | var icon: string = attrs.icon 97 | var href: string = attrs.href 98 | 99 | if (attrs.iconClass === "font-brands") { 100 | className = classes.tabIconBrands 101 | } 102 | 103 | var title = this.props.config.linkTabTitle(el) 104 | } 105 | 106 | const iconRender = () => { 107 | if (href) { 108 | return ( 109 | 110 | ) 111 | } 112 | return icon 113 | } 114 | 115 | return ( 116 | {iconRender()}} 117 | key={"tab-" + i} label={{title}} {...a11yProps(i)} /> 118 | ) 119 | }) 120 | } 121 | 122 | private dataFields(el: Node | Link): Array { 123 | if (el.type === 'node') { 124 | return this.props.config.nodeDataFields() 125 | } else { 126 | return this.props.config.linkDataFields() 127 | } 128 | } 129 | 130 | renderTabPanels(classes: any) { 131 | const dataByPath = (data: any, path: string): any => { 132 | for (let key of path.split(".")) { 133 | data = data[key] 134 | if (!data) { 135 | break 136 | } 137 | } 138 | 139 | return data 140 | } 141 | 142 | return this.props.selection.map((el: Node | Link, i: number) => { 143 | if (this.state.tab !== i) { 144 | return null 145 | } 146 | 147 | return ( 148 | 149 |
150 | 151 | this.props.onClose && this.props.onClose(el)} 153 | color="inherit"> 154 | 155 | 156 | 157 | 158 | this.props.onLocation && this.props.onLocation(el)} 160 | color="inherit"> 161 | 162 | 163 | 164 | {this.props.buttonsContent && this.props.buttonsContent(el)} 165 |
166 | {this.props.panelsContent && this.props.panelsContent(el)} 167 | 168 | {this.dataFields(el).map(entry => { 169 | var data = el.data 170 | var exclude = new Array() 171 | 172 | if (entry.field) { 173 | data = dataByPath(el.data, entry.field) 174 | } else if (entry.data) { 175 | data = entry.data(el) 176 | } 177 | 178 | exclude = this.dataFields(el).filter(cfg => cfg.field).map(cfg => { 179 | if (entry.field) { 180 | return cfg.field.replace(entry.field + ".", "") 181 | } else { 182 | return cfg.field 183 | } 184 | }) 185 | 186 | if (data) { 187 | var title = entry.title || entry.field || "General" 188 | var sortKeys = entry.sortKeys ? entry.sortKeys(data) : null 189 | var filterKeys = entry.filterKeys ? entry.filterKeys(data) : null 190 | 191 | var suffix = title.toLowerCase().replace(" ", "-") 192 | return ( 193 | 197 | ) 198 | } 199 | }) 200 | } 201 | 202 |
203 | ) 204 | }) 205 | } 206 | 207 | onTabChange(event: React.ChangeEvent<{}>, value: number) { 208 | this.setState({ tab: value, gremlin: "" }) 209 | } 210 | 211 | render() { 212 | const { classes } = this.props 213 | 214 | return ( 215 |
216 | 223 | {this.renderTabs(classes)} 224 | 225 |
226 | {this.renderTabPanels(classes)} 227 |
228 |
229 | ) 230 | } 231 | } 232 | 233 | export const mapStateToProps = (state: AppState) => ({ 234 | selection: state.selection, 235 | revision: state.selectionRevision 236 | }) 237 | 238 | export const mapDispatchToProps = ({ 239 | }) 240 | 241 | export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(SelectionPanel)) 242 | -------------------------------------------------------------------------------- /src/SelectionPanelStyles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import { createStyles, Theme } from '@material-ui/core' 19 | 20 | export const styles = (theme: Theme) => createStyles({ 21 | rightPanelPaperContent: { 22 | padding: theme.spacing(2), 23 | width: `100%`, 24 | height: `100%`, 25 | maxHeight: 'calc(100% - 90px)' 26 | }, 27 | jsonTree: { 28 | backgroundColor: 'unset' 29 | }, 30 | tabs: { 31 | height: `100%` 32 | }, 33 | tabIconFree: { 34 | fontFamily: `"Font Awesome 5 Free" !important`, 35 | fontWeight: 900, 36 | fontSize: 24, 37 | marginBottom: `0 !important` 38 | }, 39 | tabIconBrands: { 40 | fontFamily: `"Font Awesome 5 Brands" !important`, 41 | fontWeight: 900, 42 | fontSize: 24, 43 | marginBottom: `0 !important` 44 | }, 45 | tabTitle: { 46 | fontSize: 18 47 | }, 48 | tabActions: { 49 | display: 'flex', 50 | flexDirection: 'row-reverse', 51 | marginBottom: theme.spacing(1) 52 | }, 53 | gremlinExpr: { 54 | fontSize: '16px', 55 | padding: 0 56 | }, 57 | actionPanel: { 58 | marginBottom: 16 59 | }, 60 | iconImg: { 61 | maxWidth: 32, 62 | maxHeight: 32, 63 | verticalAlign: 'middle' 64 | } 65 | }) -------------------------------------------------------------------------------- /src/StdDataNormalizer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import { GoogleChartWrapperChartType } from 'react-google-charts/dist/types' 19 | 20 | export interface Graph { 21 | type: GoogleChartWrapperChartType 22 | data: Array> 23 | } 24 | 25 | export interface Column { 26 | name: string 27 | options: { 28 | filterList: Array 29 | display: string 30 | customBodyRenderLite?: (dataIndex: number, rowIndex: number) => any 31 | } 32 | } 33 | 34 | interface Row { 35 | entries: Map 36 | details: any 37 | } 38 | 39 | export class Result { 40 | private _columns: Array 41 | private _rows: Array 42 | graph?: Graph 43 | 44 | private colIndexes: Map 45 | 46 | constructor() { 47 | this._columns = new Array() 48 | this._rows = new Array() 49 | 50 | this.colIndexes = new Map() 51 | } 52 | 53 | addColumn(name: string) { 54 | let index = this.colIndexes.get(name) 55 | if (index === undefined) { 56 | this.colIndexes.set(name, this._columns.length) 57 | this._columns.push({ "name": name, options: { filterList: new Array(), display: 'true' } }) 58 | } 59 | } 60 | 61 | columnIndex(name: string): number | undefined { 62 | return this.colIndexes.get(name) 63 | } 64 | 65 | newRow(): Row { 66 | return { entries: new Map(), details: null } 67 | } 68 | 69 | addRow(row: Row) { 70 | if (row.entries.size) { 71 | for (let key of row.entries.keys()) { 72 | this.addColumn(key) 73 | } 74 | 75 | this._rows.push(row) 76 | } 77 | } 78 | 79 | // sort only if the format in key-value 80 | sortRowsByKeys(keys: Array) { 81 | if (this._columns.length === 2 && this._columns[0].name === "Key") { 82 | this._rows = this._rows.sort((a, b) => { 83 | var ka = a.entries.get("Key"), kb = b.entries.get("Key") 84 | var ia = keys.indexOf(ka), ib = keys.indexOf(kb) 85 | 86 | if (ia === ib) return ka.localeCompare(kb) // -1 == -1 87 | if (ia === -1) return 1 88 | if (ib === -1) return -1 89 | return ia - ib 90 | }) 91 | } 92 | } 93 | 94 | get rows(): Array> { 95 | var rows = new Array>() 96 | for (let row of this._rows) { 97 | let plain = new Array() 98 | for (let i = 0; i != this.colIndexes.size; i++) { 99 | plain[i] = "" 100 | } 101 | 102 | row.entries.forEach((value, key) => { 103 | var index = this.columnIndex(key) 104 | if (index === undefined) { 105 | return 106 | } 107 | plain[index] = value 108 | }) 109 | 110 | rows.push(plain) 111 | } 112 | 113 | return rows 114 | } 115 | 116 | get details(): Map { 117 | var details = new Map() 118 | 119 | this._rows.forEach((row, index) => { 120 | if (row.details) { 121 | details.set(index, row.details) 122 | } 123 | }) 124 | 125 | return details 126 | } 127 | 128 | get columns(): Array { 129 | return this._columns 130 | } 131 | } 132 | 133 | export class DataNormalizer { 134 | 135 | normalizer: ((any) => any) | null 136 | graph: ((any) => Graph) | null 137 | exclude: Array | null 138 | sortKeys: Array | null 139 | 140 | constructor(normalizer?: (data: any) => any, graph?: ((data: any) => Graph), exclude?: Array, sortKeys?: Array) { 141 | this.normalizer = normalizer || null 142 | this.graph = graph || null 143 | this.exclude = exclude || [] 144 | this.sortKeys = sortKeys || [] 145 | } 146 | 147 | private normalizeMap(data: any, result: Result) { 148 | for (let attr in data) { 149 | if (this.exclude && this.exclude.includes(attr)) { 150 | continue 151 | } 152 | 153 | let row = result.newRow() 154 | 155 | let value = data[attr] 156 | switch (typeof value) { 157 | case "object": 158 | row.entries.set("Key", attr) 159 | row.entries.set("Value", "") 160 | row.details = value 161 | break 162 | case "boolean": 163 | row.entries.set("Key", attr) 164 | row.entries.set("Value", value ? "true" : "false") 165 | break 166 | default: 167 | row.entries.set("Key", attr) 168 | row.entries.set("Value", value) 169 | break 170 | } 171 | 172 | result.addRow(row) 173 | } 174 | 175 | return result 176 | } 177 | 178 | private normalizeScalarArray(data: any, result: Result) { 179 | for (let value of data) { 180 | let row = result.newRow() 181 | 182 | if (typeof value === "boolean") { 183 | row.entries.set("Value", value ? "true" : "false") 184 | } else { 185 | row.entries.set("Value", value) 186 | } 187 | result.addRow(row) 188 | } 189 | 190 | return result 191 | } 192 | 193 | private normalizeObjectArray(data: any, result: Result) { 194 | for (let value of data) { 195 | let row = result.newRow() 196 | 197 | for (let attr in value) { 198 | var type = typeof value[attr] 199 | if (type === "boolean") { 200 | row.entries.set(attr, value ? "true" : "false") 201 | } if (type === "string" || type === "number") { 202 | row.entries.set(attr, value[attr] === null ? "" : value[attr]) 203 | } 204 | } 205 | 206 | result.addRow(row) 207 | } 208 | 209 | return result 210 | } 211 | 212 | private normalizeArray(data: any, result: Result) { 213 | if ((data as Array).some(value => typeof value === "object")) { 214 | return this.normalizeObjectArray(data, result) 215 | } 216 | 217 | this.normalizeScalarArray(data, result) 218 | } 219 | 220 | private normalizeData(data: any, result: Result) { 221 | if (Array.isArray(data)) { 222 | this.normalizeArray(data, result) 223 | } else if (typeof data === "object") { 224 | this.normalizeMap(data, result) 225 | } 226 | } 227 | 228 | normalize(data: any): Result { 229 | var result = new Result() 230 | 231 | if (this.graph) { 232 | result.graph = this.graph(data) 233 | } 234 | 235 | if (this.normalizer) { 236 | data = this.normalizer(data) 237 | } 238 | 239 | this.normalizeData(data, result) 240 | 241 | if (this.sortKeys) { 242 | result.sortRowsByKeys(this.sortKeys) 243 | } 244 | 245 | return result 246 | } 247 | } -------------------------------------------------------------------------------- /src/StdDataPanel.css: -------------------------------------------------------------------------------- 1 | .MuiAccordionSummary-content{ 2 | align-items: center; 3 | } -------------------------------------------------------------------------------- /src/StdDataPanel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from "react" 19 | import Accordion from '@material-ui/core/Accordion' 20 | import AccordionSummary from '@material-ui/core/AccordionSummary' 21 | import AccordionDetails from '@material-ui/core/AccordionDetails' 22 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore' 23 | import Typography from '@material-ui/core/Typography' 24 | import { withStyles } from '@material-ui/core/styles' 25 | 26 | import { DataViewer } from './StdDataViewer' 27 | import { DataNormalizer, Result, Graph } from './StdDataNormalizer' 28 | import { styles } from './StdDataPanelStyles' 29 | import './StdDataPanel.css' 30 | 31 | interface Props { 32 | title: string 33 | icon?: string 34 | iconClass?: string 35 | data?: any 36 | fetch?: () => Promise 37 | classes: any 38 | defaultExpanded?: boolean 39 | normalizer?: (data: any) => any 40 | graph?: (data: any) => Graph 41 | exclude?: Array 42 | sortKeys?: Array 43 | filterKeys?: Array 44 | defaultColumns?: Array 45 | revision: number 46 | deletable?: boolean 47 | onDelete?: (data: Array>) => void 48 | customRenders?: Map any> 49 | } 50 | 51 | interface State { 52 | data?: any 53 | result?: Result 54 | filterKeys?: Array 55 | columns?: Array 56 | error?: string 57 | } 58 | 59 | class DataPanel extends React.Component { 60 | 61 | state: State 62 | 63 | constructor(props) { 64 | super(props) 65 | 66 | this.state = { 67 | data: props.data, 68 | columns: props.columns, 69 | } 70 | } 71 | 72 | static normalizeData(data: any, normalizer?: (data: any) => any, graph?: (data: any) => Graph, exclude?: Array, sortKeys?: Array): Result { 73 | var dataNormalizer = new DataNormalizer(normalizer, graph, exclude, sortKeys) 74 | return dataNormalizer.normalize(data) 75 | } 76 | 77 | static normalizeFilterKeys(data: any, filterKeys: Array | undefined): Array | undefined { 78 | if (!filterKeys) { 79 | return 80 | } 81 | return filterKeys.filter(key => Boolean(data[key])) 82 | } 83 | 84 | static getDerivedStateFromProps(props, state) { 85 | return { 86 | data: props.data, 87 | columns: props.columns, 88 | } 89 | } 90 | 91 | componentDidMount() { 92 | if (this.props.defaultExpanded) { 93 | this.refreshData() 94 | } 95 | } 96 | 97 | componentDidUpdate(prevProps) { 98 | if (prevProps.revision !== this.props.revision) { 99 | this.refreshData() 100 | } 101 | } 102 | 103 | private refreshData() { 104 | if (this.state.data) { 105 | this.setState({ 106 | result: DataPanel.normalizeData(this.state.data, this.props.normalizer, this.props.graph, this.props.exclude, this.props.sortKeys), 107 | filterKeys: DataPanel.normalizeFilterKeys(this.state.data, this.props.filterKeys), 108 | }) 109 | } else if (this.props.fetch) { 110 | this.props.fetch().then(data => { 111 | if (data) { 112 | this.setState({ 113 | result: DataPanel.normalizeData(data, this.props.normalizer, this.props.graph, this.props.exclude, this.props.sortKeys), 114 | filterKeys: DataPanel.normalizeFilterKeys(data, this.props.filterKeys), 115 | error: undefined 116 | }) 117 | } else { 118 | this.setState({ error: "No data available" }) 119 | } 120 | }).catch(err => { 121 | this.setState({ error: err.message }) 122 | }) 123 | } 124 | } 125 | 126 | private onExpandChange(event: object, expanded: boolean) { 127 | if (expanded) { 128 | this.refreshData() 129 | } 130 | } 131 | 132 | private onFilterReset() { 133 | this.refreshData() 134 | } 135 | 136 | render() { 137 | const { classes } = this.props 138 | 139 | const iconClass = this.props.iconClass === "font-brands" ? classes.panelIconBrands : classes.panelIconFree 140 | 141 | var details = Loading... 142 | if (this.state.error) { 143 | details = {this.state.error} 144 | } else if (this.state.result && this.state.result.rows.length) { 145 | details = 149 | } 150 | 151 | return ( 152 | 153 | } 155 | aria-controls="panel1a-content" 156 | id="panel1a-header"> 157 | {this.props.icon} 158 | {this.props.title} 159 | 160 | 161 | {details} 162 | 163 | 164 | ) 165 | } 166 | } 167 | 168 | export default withStyles(styles)(DataPanel) -------------------------------------------------------------------------------- /src/StdDataPanelStyles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import { createStyles, Theme } from '@material-ui/core' 19 | 20 | export const styles = (theme: Theme) => createStyles({ 21 | panelIconFree: { 22 | fontFamily: `"Font Awesome 5 Free" !important`, 23 | fontWeight: 900, 24 | fontSize: 20, 25 | marginBottom: `0 !important`, 26 | paddingRight: 8 27 | }, 28 | panelIconBrands: { 29 | fontFamily: `"Font Awesome 5 Brands" !important`, 30 | fontWeight: 900, 31 | fontSize: 20, 32 | marginBottom: `0 !important`, 33 | paddingRight: 8 34 | } 35 | }) -------------------------------------------------------------------------------- /src/StdDataViewer.css: -------------------------------------------------------------------------------- 1 | .MuiAccordionDetails-root { 2 | display: block !important; 3 | padding: 0 !important; 4 | } 5 | 6 | .MuiAccordionPanelDetails-root .MuiPaper-root { 7 | box-shadow: none !important; 8 | } 9 | 10 | .MuiAccordionSummary-root { 11 | background-color: #e8e8e8 !important; 12 | } 13 | 14 | .not-expandable button { 15 | visibility: hidden; 16 | } -------------------------------------------------------------------------------- /src/StdDataViewer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from 'react' 19 | import MUIDataTable from 'mui-datatables' 20 | import TableRow from "@material-ui/core/TableRow" 21 | import TableCell from "@material-ui/core/TableCell" 22 | import { Chart } from 'react-google-charts' 23 | import JSONTree from 'react-json-tree' 24 | import FilterNoneIcon from '@material-ui/icons/FilterNone' 25 | import IconButton from '@material-ui/core/IconButton' 26 | import Tooltip from '@material-ui/core/Tooltip' 27 | import DeleteIcon from '@material-ui/icons/Delete' 28 | 29 | import { Column, Graph } from './StdDataNormalizer' 30 | import './StdDataViewer.css' 31 | 32 | interface Props { 33 | title?: string 34 | columns: Array 35 | data: Array> 36 | graph?: Graph 37 | details: Map 38 | filterKeys?: Array 39 | onFilterReset?: () => void 40 | defaultColumns?: Array 41 | deletable?: boolean 42 | onDelete?: (data: Array>) => void 43 | customRenders?: Map any> 44 | } 45 | 46 | interface State { 47 | sortOrder: Map 48 | sortField: string 49 | filterList: Map> 50 | graph?: Graph 51 | rowsExpanded: Array 52 | rowsSelected: Array 53 | } 54 | 55 | export class DataViewer extends React.Component { 56 | 57 | state: State 58 | applyDefaultColumns: boolean 59 | 60 | constructor(props) { 61 | super(props) 62 | 63 | this.applyDefaultColumns = true 64 | 65 | this.state = { 66 | sortOrder: new Map(), 67 | sortField: "", 68 | filterList: new Map>(), 69 | rowsExpanded: new Array(), 70 | rowsSelected: new Array() 71 | } 72 | } 73 | 74 | static getDerivedStateFromProps(props, state) { 75 | if (props.defaultColumns) { 76 | state.defaultColumns = props.defaultColumns 77 | } 78 | 79 | if (props.graph) { 80 | if (state.graph) { 81 | state.graph.data = state.graph.data.concat(props.graph.data.slice(1)) 82 | } else { 83 | state.graph = props.graph 84 | } 85 | } 86 | 87 | return state 88 | } 89 | 90 | private resetFilter() { 91 | this.setState({ filterList: new Map>() }) 92 | 93 | if (this.props.onFilterReset) { 94 | this.props.onFilterReset() 95 | } 96 | } 97 | 98 | render() { 99 | var options: any = { 100 | filterType: 'multiselect', 101 | selectableRows: 'none', 102 | responsive: 'vertical', 103 | print: false, 104 | download: false, 105 | customToolbar: () => { 106 | return ( 107 | 108 | 109 | 110 | 111 | 112 | ) 113 | }, 114 | rowsSelected: this.state.rowsSelected, 115 | setRowProps: (row, dataIndex) => { 116 | if (!this.props.details.get(dataIndex)) { 117 | return { "className": "not-expandable" } 118 | } 119 | return {} 120 | }, 121 | expandableRows: true, 122 | expandableRowsHeader: false, 123 | expandableRowsOnClick: true, 124 | isRowExpandable: (dataIndex, expandedRows) => { 125 | if (this.props.details.get(dataIndex)) { 126 | return true 127 | } 128 | return false 129 | }, 130 | renderExpandableRow: (rowData, rowMeta) => { 131 | const colSpan = rowData.length 132 | return ( 133 | 134 | 135 | 136 | 138 | 139 | 140 | ) 141 | }, 142 | rowsExpanded: this.state.rowsExpanded, 143 | onRowExpansionChange: (currentRowsExpanded, allRowsExpanded) => { 144 | this.setState({ rowsExpanded: allRowsExpanded.map(entry => entry.dataIndex) }) 145 | }, 146 | onColumnSortChange: (field: string, direction: string) => { 147 | this.setState({ sortField: field }) 148 | }, 149 | onColumnViewChange: (column: string, action: string) => { 150 | }, 151 | onFilterChange: (field: string, filterList: Array) => { 152 | var newList = new Array() 153 | 154 | filterList.forEach((a: Array) => { 155 | if (a.length) { 156 | newList = newList.concat(a) 157 | } 158 | }) 159 | 160 | this.state.filterList.set(field, newList) 161 | this.setState({ filterList: this.state.filterList }) 162 | }, 163 | sortOrder: this.state.sortOrder 164 | } 165 | 166 | if (this.props.deletable) { 167 | options.selectableRows = 'multiple' 168 | options.onRowSelectionChange = (currentRowsSelected, allRowsSelected, rowsSelected) => { 169 | this.state.rowsSelected = rowsSelected 170 | this.setState({ rowsSelected: this.state.rowsSelected }) 171 | } 172 | options.customToolbarSelect = (selectedRows, displayData, setSelectedRows) => { 173 | var data = new Array>() 174 | selectedRows.data.forEach((el: any) => { 175 | let row: any = this.props.data[el.dataIndex] 176 | 177 | let values = new Map() 178 | for (let i = 0; i != this.props.columns.length; i++) { 179 | values[this.props.columns[i].name] = row[i] 180 | } 181 | data.push(values) 182 | }); 183 | 184 | return ( 185 | 186 | this.props.onDelete && this.props.onDelete(data)}> 187 | 188 | 189 | 190 | ) 191 | } 192 | } 193 | 194 | // re-apply sort and filter if need 195 | for (let column of this.props.columns) { 196 | if (this.applyDefaultColumns && this.props.defaultColumns) { 197 | if (!this.props.defaultColumns.includes(column.name)) { 198 | column.options.display = 'false' 199 | } 200 | } 201 | 202 | // use value from config first 203 | if (column.name === "Key" && this.props.filterKeys) { 204 | column.options.filterList = this.props.filterKeys 205 | } 206 | 207 | let filterList = this.state.filterList.get(column.name) 208 | if (filterList) { 209 | column.options.filterList = filterList 210 | } 211 | 212 | var cb = this.props.customRenders?.get(column.name) 213 | if (cb) { 214 | column.options.customBodyRenderLite = (dataIndex: number, rowIndex: number): any => { 215 | var name = this.props.columns[dataIndex].name 216 | var value = this.props.data[rowIndex][dataIndex] 217 | 218 | var cb = this.props.customRenders?.get(column.name) 219 | if (cb) { 220 | return cb(value) 221 | } 222 | return value 223 | } 224 | } 225 | } 226 | this.applyDefaultColumns = false 227 | 228 | return ( 229 | 230 | 235 | { 236 | this.state.graph && 237 | Loading Chart} 241 | data={this.state.graph.data} 242 | options={{ 243 | chart: { 244 | }, 245 | }} 246 | /> 247 | } 248 | 249 | ) 250 | } 251 | } -------------------------------------------------------------------------------- /src/Store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import { createStore } from 'redux' 19 | 20 | import { Node, Link } from './Topology' 21 | 22 | export const SELECT_ELEMENT = 'SELECT_ELEMENT' 23 | export const UNSELECT_ELEMENT = 'UNSELECT_ELEMENT' 24 | export const BUMP_REVISION = 'BUMP_REVISION' 25 | export const OPEN_SESSION = 'OPEN_SESSION' 26 | export const CLOSE_SESSION = 'CLOSE_SESSION' 27 | 28 | interface selectElementAction { 29 | type: typeof SELECT_ELEMENT 30 | payload: Node | Link 31 | } 32 | 33 | interface unselectElementAction { 34 | type: typeof UNSELECT_ELEMENT 35 | payload: Node | Link 36 | } 37 | 38 | interface bumpRevisionAction { 39 | type: typeof BUMP_REVISION 40 | payload: string 41 | } 42 | 43 | export interface session { 44 | endpoint: string 45 | username: string 46 | token: string 47 | permissions: any 48 | persistent: boolean 49 | } 50 | 51 | interface openSessionAction { 52 | type: typeof OPEN_SESSION 53 | payload: session 54 | } 55 | 56 | interface closeSessionAction { 57 | type: typeof CLOSE_SESSION 58 | payload: null 59 | } 60 | 61 | export function selectElement(node: Node | Link): selectElementAction { 62 | return { 63 | type: SELECT_ELEMENT, 64 | payload: node 65 | } 66 | } 67 | 68 | export function unselectElement(node: Node | Link): unselectElementAction { 69 | return { 70 | type: UNSELECT_ELEMENT, 71 | payload: node 72 | } 73 | } 74 | 75 | export function bumpRevision(id: string): bumpRevisionAction { 76 | return { 77 | type: BUMP_REVISION, 78 | payload: id 79 | } 80 | } 81 | 82 | export function openSession(endpoint: string, username: string, token: string, permissions: any, persistent: boolean): openSessionAction { 83 | return { 84 | type: OPEN_SESSION, 85 | payload: { 86 | endpoint: endpoint, 87 | username: username, 88 | token: token, 89 | permissions: permissions, 90 | persistent: persistent 91 | } 92 | } 93 | } 94 | 95 | export function closeSession(): closeSessionAction { 96 | return { 97 | type: CLOSE_SESSION, 98 | payload: null 99 | } 100 | } 101 | 102 | export type ActionTypes = selectElementAction | unselectElementAction | bumpRevisionAction | openSessionAction | closeSessionAction 103 | 104 | const emptySession = { 105 | endpoint: `${window.location.protocol}//${window.location.hostname}:${window.location.port}`, 106 | username: "", 107 | token: "", 108 | permissions: {}, 109 | persistent: false 110 | } 111 | 112 | const loadSession = (): session => { 113 | try { 114 | const serializedSession = localStorage.getItem('session') 115 | if (serializedSession === null) { 116 | return emptySession 117 | } 118 | return JSON.parse(serializedSession) 119 | } catch (err) { 120 | return emptySession 121 | } 122 | } 123 | 124 | const initialState = { 125 | selection: new Array(), 126 | selectionRevision: 0, 127 | session: loadSession(), 128 | } 129 | 130 | function appReducer(state = initialState, action: ActionTypes) { 131 | switch (action.type) { 132 | case SELECT_ELEMENT: 133 | var selection = state.selection.filter(d => action.payload.id !== d.id) 134 | selection.push(action.payload) 135 | return { 136 | ...state, 137 | selection: selection 138 | } 139 | case UNSELECT_ELEMENT: 140 | var selection = state.selection.filter(d => action.payload.id !== d.id) 141 | return { 142 | ...state, 143 | selection: selection 144 | } 145 | case BUMP_REVISION: 146 | if (state.selection.some(el => el.id === action.payload)) { 147 | return { 148 | ...state, 149 | selectionRevision: state.selectionRevision + 1 150 | } 151 | } 152 | return state 153 | case OPEN_SESSION: 154 | if (action.payload.persistent) { 155 | localStorage.setItem('session', JSON.stringify(action.payload)) 156 | } 157 | 158 | return { 159 | ...state, 160 | session: action.payload 161 | } 162 | case CLOSE_SESSION: 163 | localStorage.removeItem('session') 164 | 165 | return { 166 | ...state, 167 | session: emptySession 168 | } 169 | default: 170 | return state 171 | } 172 | } 173 | 174 | export type AppState = ReturnType 175 | 176 | export const store = createStore(appReducer) -------------------------------------------------------------------------------- /src/Tabs.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from 'react' 19 | import Typography from '@material-ui/core/Typography' 20 | 21 | export interface TabPanelProps { 22 | children?: React.ReactNode 23 | index: any 24 | value: any 25 | } 26 | 27 | export function TabPanel(props: TabPanelProps) { 28 | const { children, value, index, ...other } = props 29 | 30 | return ( 31 | 40 | ) 41 | } 42 | 43 | export function a11yProps(index: any) { 44 | return { 45 | id: `tab-${index}`, 46 | 'aria-controls': `tabpanel-${index}`, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/TimetravelPanel.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from "react" 19 | import { connect } from 'react-redux' 20 | import { withStyles } from '@material-ui/core/styles' 21 | import { AppState, session } from './Store' 22 | import { styles } from './TimetravelPanelStyles' 23 | import Timeline from '@material-ui/lab/Timeline' 24 | import TimelineItem from '@material-ui/lab/TimelineItem' 25 | import TimelineSeparator from '@material-ui/lab/TimelineSeparator' 26 | import TimelineConnector from '@material-ui/lab/TimelineConnector' 27 | import TimelineContent from '@material-ui/lab/TimelineContent'; 28 | import TimelineOppositeContent from '@material-ui/lab/TimelineOppositeContent' 29 | import TimelineDot from '@material-ui/lab/TimelineDot' 30 | import Paper from '@material-ui/core/Paper' 31 | import TextField from '@material-ui/core/TextField' 32 | import Autocomplete from '@material-ui/lab/Autocomplete' 33 | import { Typography } from '@material-ui/core' 34 | import Chip from '@material-ui/core/Chip' 35 | import Grid from '@material-ui/core/Grid' 36 | 37 | import { Node } from './Topology' 38 | import ConfigReducer from './Config' 39 | import { Configuration } from './api/configuration' 40 | import { TopologyApi } from './api' 41 | 42 | const moment = require('moment') 43 | 44 | interface Props { 45 | classes: any 46 | session: session 47 | config: ConfigReducer 48 | onNavigate: (date: Date) => void 49 | } 50 | 51 | interface State { 52 | data: Array 53 | nodeType: string 54 | timeContext: Date 55 | } 56 | 57 | class TimetravelPanel extends React.Component { 58 | 59 | state: State 60 | nodeType: string 61 | timeContext: Date 62 | 63 | constructor(props) { 64 | super(props) 65 | 66 | const localeIso = () => { 67 | const date = new Date() 68 | const offsetMs = date.getTimezoneOffset() * 60 * 1000 69 | const msLocal = date.getTime() - offsetMs 70 | return new Date(msLocal) 71 | } 72 | 73 | this.timeContext = new Date() 74 | 75 | this.state = { 76 | data: new Array(), 77 | nodeType: "", 78 | timeContext: localeIso() 79 | } 80 | this.refreshData() 81 | } 82 | 83 | refreshData() { 84 | var conf = new Configuration({ basePath: this.props.session.endpoint + "/api", accessToken: this.props.session.token }) 85 | var api = new TopologyApi(conf) 86 | 87 | var nodes = "G" 88 | 89 | if (this.timeContext) { 90 | nodes += ".At(" + this.timeContext.getTime() + ", 300)" 91 | } 92 | 93 | nodes += ".V().HasNot('@DeleteAt')" 94 | 95 | if (this.nodeType) { 96 | nodes += ".Has('Type', '" + this.nodeType + "')" 97 | } 98 | 99 | api.searchTopology({ GremlinQuery: nodes + `.Dedup().Valuemap('Name', 'Type', '@CreatedAt')` }).then((result: Array) => { 100 | if (result) { 101 | this.setState({ 102 | data: result.sort((a, b) => { 103 | let n = b['@CreatedAt'] - a['@CreatedAt'] 104 | if (n) return n 105 | return b['Name'].localeCompare(a['Name']) 106 | }) 107 | }) 108 | } else { 109 | this.setState({ data: [] }) 110 | } 111 | }) 112 | } 113 | 114 | render() { 115 | const { classes } = this.props 116 | 117 | const dateRender = (data: any) => { 118 | var createdAt = moment(data["@CreatedAt"]).format("L LTS") 119 | return ( 120 | 121 | Navigate to: 122 | { 124 | let date = moment(createdAt).toDate() 125 | if (this.props.onNavigate) { 126 | this.props.onNavigate(date) 127 | } 128 | } 129 | } /> 130 | 131 | ) 132 | } 133 | 134 | const iconRender = (data: any) => { 135 | var className = classes.tabIconFree 136 | 137 | var id = data["@ID"] 138 | 139 | delete data['@ID'] 140 | delete data['@CreateAt'] 141 | 142 | var node = new Node(id, [], data) 143 | 144 | var attrs = this.props.config.nodeAttrs(node) 145 | var icon: string = attrs.icon 146 | var href: string = attrs.href 147 | 148 | if (attrs.iconClass === "font-brands") { 149 | className = classes.tabIconBrands 150 | } 151 | 152 | if (href) { 153 | return ( 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | ) 162 | } 163 | return ( 164 | 165 | 166 | 167 | {icon} 168 | 169 | 170 | 171 | ) 172 | } 173 | 174 | const uniq = (value: any, index: number, self) => { 175 | return self.indexOf(value) === index; 176 | } 177 | 178 | return ( 179 |
180 |
181 | 182 | 183 | { 188 | this.nodeType = newValue 189 | this.setState({ nodeType: this.nodeType }) 190 | 191 | this.refreshData() 192 | }} 193 | onKeyDown={(event) => { 194 | if (event.key === 'Enter') { 195 | event.preventDefault() 196 | 197 | this.refreshData() 198 | } 199 | }} 200 | options={this.state.data.map((node: any) => node.Type).filter(uniq)} 201 | renderInput={(params) => ( 202 | 203 | )} 204 | /> 205 | 206 | 207 | { 213 | const { value } = event.target 214 | 215 | this.timeContext = moment(value).toDate() 216 | this.setState({ timeContext: this.timeContext }) 217 | 218 | this.refreshData() 219 | }} 220 | margin="normal" 221 | className={classes.textField} 222 | InputLabelProps={{ 223 | shrink: true, 224 | }} 225 | /> 226 | 227 | 228 |
229 | 230 | {this.state.data.map((node: any, i: number) => 231 | 232 | 233 | {dateRender(node)} 234 | 235 | 236 | {iconRender(node)} 237 | {(i < this.state.data.length - 1) && 238 | 239 | } 240 | 241 | 242 | 243 | 245 | {node["Name"]} 246 | 247 | {node["Type"]} 248 | 249 | 250 | 251 | )} 252 | 253 |
254 |
255 |
256 | ) 257 | } 258 | } 259 | 260 | export const mapStateToProps = (state: AppState) => ({ 261 | session: state.session 262 | }) 263 | 264 | export const mapDispatchToProps = ({}) 265 | 266 | export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(TimetravelPanel)) 267 | -------------------------------------------------------------------------------- /src/TimetravelPanelStyles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import { createStyles, Theme } from '@material-ui/core' 19 | 20 | export const styles = (theme: Theme) => createStyles({ 21 | rightPanelPaperContent: { 22 | padding: theme.spacing(2), 23 | width: `100%`, 24 | height: `100%`, 25 | maxHeight: 'calc(100% - 60px)' 26 | }, 27 | panel: { 28 | height: `100%` 29 | }, 30 | title: { 31 | marginLeft: 5, 32 | marginRight: 5 33 | }, 34 | connector: { 35 | height: 25 36 | }, 37 | root: { 38 | paddingLeft: 0, 39 | paddingRight: 0, 40 | }, 41 | tabIconFree: { 42 | fontFamily: `"Font Awesome 5 Free" !important`, 43 | fontWeight: 900, 44 | fontSize: 24, 45 | marginBottom: `0 !important`, 46 | minWidth: 34, 47 | textAlign: 'center' 48 | }, 49 | tabIconBrands: { 50 | fontFamily: `"Font Awesome 5 Brands" !important`, 51 | fontWeight: 900, 52 | fontSize: 24, 53 | marginBottom: `0 !important`, 54 | minWidth: 34, 55 | textAlign: 'center' 56 | }, 57 | textField: { 58 | "& .MuiInputBase-root": { 59 | display: "grid" 60 | } 61 | }, 62 | content: { 63 | overflow: "auto", 64 | height: "100%" 65 | } 66 | }) -------------------------------------------------------------------------------- /src/Tools.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | 19 | export default class Tools { 20 | 21 | static prettyBytes(value: number) { 22 | var g = Math.floor(value / 1000000000) 23 | var m = Math.floor((value - g * 1000000000) / 1000000) 24 | var k = Math.floor((value - g * 1000000000 - m * 1000000) / 1000) 25 | var b = value - g * 1000000000 - m * 1000000 - k * 1000 26 | 27 | if (g) return g + "Gb (" + value.toLocaleString() + " bytes)" 28 | if (m) return m + "Mb (" + value.toLocaleString() + " bytes)" 29 | if (k) return k + "Kb (" + value.toLocaleString() + " bytes)" 30 | 31 | return b.toLocaleString() + " bytes" 32 | } 33 | 34 | static prettyBandwidth(value: number) { 35 | var g = Math.floor(value / 1000000000) 36 | var m = Math.floor((value - g * 1000000000) / 1000000) 37 | var k = Math.floor((value - g * 1000000000 - m * 1000000) / 1000) 38 | var b = value - g * 1000000000 - m * 1000000 - k * 1000 39 | 40 | if (g) return g + "Gbit/s" 41 | if (m) return m + "Mbit/s" 42 | if (k) return k + "Kbit/s" 43 | 44 | return b.toLocaleString() + " bit/s" 45 | } 46 | } -------------------------------------------------------------------------------- /src/Topology.css: -------------------------------------------------------------------------------- 1 | svg { 2 | cursor: default; 3 | } 4 | 5 | .levels { 6 | fill: none; 7 | } 8 | 9 | .level-zone { 10 | stroke: #888; 11 | stroke-dasharray: 5, 10; 12 | fill: #f6f6f6; 13 | } 14 | 15 | .level-label rect { 16 | fill: #e4e4e4; 17 | } 18 | 19 | .level-label text { 20 | fill:#333; 21 | font-weight: 500; 22 | } 23 | 24 | .hiera-links { 25 | fill: none; 26 | stroke: #333; 27 | stroke-width: 2; 28 | } 29 | 30 | .link-overlays { 31 | fill: none; 32 | stroke: #ffeb3b; 33 | stroke-width: 20; 34 | marker-start: url(#link-overlay-marker); 35 | marker-end: url(#link-overlay-marker); 36 | } 37 | 38 | .link-overlay-selected { 39 | stroke: #5097c7; 40 | marker-start: url(#link-overlay-selected-marker); 41 | marker-end: url(#link-overlay-selected-marker); 42 | } 43 | 44 | .link-overlay-marker { 45 | fill: #ffeb3b; 46 | } 47 | 48 | .link-overlay-selected-marker { 49 | fill: #5097c7; 50 | } 51 | 52 | .links { 53 | fill: none; 54 | stroke: #0069bd; 55 | stroke-width: 3; 56 | marker-start: url(#link-marker); 57 | marker-end: url(#link-marker); 58 | } 59 | 60 | .links .directed { 61 | marker-start: unset; 62 | marker-end: url(#link-directed-marker); 63 | } 64 | 65 | .links .directed-inv { 66 | marker-start: url(#link-directed-marker); 67 | marker-end: unset; 68 | } 69 | 70 | .link-marker { 71 | fill: #003865; 72 | } 73 | 74 | .link-wraps { 75 | stroke: #000; 76 | stroke-width: 30; 77 | opacity: 0; 78 | fill: none; 79 | } 80 | 81 | .link-label { 82 | font-size: 18px; 83 | font-weight: normal; 84 | font-family: sans-serif; 85 | } 86 | 87 | .hiera-link { 88 | stroke-dasharray: 5, 10; 89 | } 90 | 91 | .node-circle { 92 | fill: #ffffff00; 93 | stroke-width: 3; 94 | stroke: #5097c7; 95 | } 96 | 97 | .node-overlay { 98 | fill: #ffeb3b; 99 | } 100 | 101 | .node-pinned { 102 | fill: rgb(55, 255, 37); 103 | } 104 | 105 | .node-disc { 106 | stroke: none; 107 | fill: #5097c7; 108 | } 109 | 110 | .node-selected .node-circle { 111 | fill: #5097c7; 112 | } 113 | 114 | .node-hexagon { 115 | stroke: #455f6b; 116 | stroke-dasharray: 5, 5; 117 | fill: #C9ADA7; 118 | } 119 | 120 | .node-icon { 121 | font-size: 24px; 122 | text-anchor: middle; 123 | font-family: 'Font Awesome 5 Free' !important; 124 | fill: #444; 125 | font-weight: 900; 126 | } 127 | 128 | .node-badge text { 129 | font-size: 18px; 130 | text-anchor: middle; 131 | font-family: 'Font Awesome 5 Free'; 132 | font-weight: 900; 133 | } 134 | 135 | .font-brands { 136 | font-family: 'Font Awesome 5 Brands' !important; 137 | } 138 | 139 | .node-pinned text { 140 | font-size: 30px; 141 | text-anchor: middle; 142 | font-family: 'Font Awesome 5 Free' !important; 143 | fill: #444; 144 | font-weight: 900; 145 | } 146 | 147 | .node-name { 148 | font-size: 22px; 149 | text-anchor: middle; 150 | font-weight: normal; 151 | font-family: sans-serif; 152 | } 153 | 154 | .node-name-wrap { 155 | fill: #fff; 156 | stroke: #666; 157 | opacity: 0.7; 158 | } 159 | 160 | .node-exco-circle { 161 | fill: #48e448; 162 | stroke: #666; 163 | 164 | } 165 | 166 | .node-exco-children { 167 | font-size: 16px; 168 | fill: #fff; 169 | text-anchor: middle; 170 | font-weight: bold; 171 | font-family: sans-serif; 172 | } 173 | 174 | .group .group-brace { 175 | stroke: #555; 176 | stroke-width: 2px; 177 | fill: none; 178 | } 179 | 180 | .group .group-brace-bg { 181 | fill: #e0e0e0; 182 | stroke: #ddd; 183 | stroke-width: 2px; 184 | } 185 | 186 | .group .group-brace-owner-bg { 187 | fill: #cac9c9; 188 | stroke: #aaa; 189 | stroke-width: 2px; 190 | } 191 | 192 | .group-button .brace-icon text { 193 | font-family: 'Font Awesome 5 Free' !important; 194 | fill: #444; 195 | font-weight: 900; 196 | font-size: 24px; 197 | } 198 | 199 | .group-button .brace-icon rect { 200 | stroke: none; 201 | fill: #eee; 202 | } 203 | 204 | .group-button .brace-offset text { 205 | fill: #eee; 206 | font-size: 16px; 207 | font-weight: bold; 208 | } 209 | 210 | .group-button .brace-offset rect { 211 | stroke: none; 212 | fill: #444; 213 | } 214 | 215 | .group-button .brace-icon:disabled { 216 | fill: #bbb; 217 | } 218 | 219 | .context-menu { 220 | font-size: 16px; 221 | } 222 | 223 | .context-menu rect { 224 | fill: #fff; 225 | stroke: #333; 226 | stroke-width: 1px; 227 | } 228 | 229 | .context-menu-item rect { 230 | fill: #eee; 231 | stroke: none; 232 | } 233 | 234 | .context-menu-item text.disabled { 235 | fill: #888; 236 | } 237 | -------------------------------------------------------------------------------- /src/api/configuration.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | /** 3 | * Skydive API 4 | * The Skydive REST API allows to communicate with a Skydive analyzer. 5 | * 6 | * OpenAPI spec version: 0.26.0 7 | * Contact: skydive-dev@redhat.com 8 | * 9 | * NOTE: This class is auto generated by the swagger code generator program. 10 | * https://github.com/swagger-api/swagger-codegen.git 11 | * Do not edit the class manually. 12 | */ 13 | 14 | 15 | export interface ConfigurationParameters { 16 | accessToken?: string | ((name: string) => string); 17 | username?: string; 18 | password?: string; 19 | basePath?: string; 20 | } 21 | 22 | export class Configuration { 23 | /** 24 | * parameter for accessToken security 25 | * @param name security name 26 | * @memberof Configuration 27 | */ 28 | accessToken?: string | ((name: string) => string); 29 | /** 30 | * parameter for basic security 31 | * 32 | * @type {string} 33 | * @memberof Configuration 34 | */ 35 | username?: string; 36 | /** 37 | * parameter for basic security 38 | * 39 | * @type {string} 40 | * @memberof Configuration 41 | */ 42 | password?: string; 43 | /** 44 | * override base path 45 | * 46 | * @type {string} 47 | * @memberof Configuration 48 | */ 49 | basePath?: string; 50 | 51 | constructor(param: ConfigurationParameters = {}) { 52 | this.accessToken = param.accessToken; 53 | this.username = param.username; 54 | this.password = param.password; 55 | this.basePath = param.basePath; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/api/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'portable-fetch'; 2 | declare module 'url'; -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | /** 3 | * Skydive API 4 | * The Skydive REST API allows to communicate with a Skydive analyzer. 5 | * 6 | * OpenAPI spec version: 0.26.0 7 | * Contact: skydive-dev@redhat.com 8 | * 9 | * NOTE: This class is auto generated by the swagger code generator program. 10 | * https://github.com/swagger-api/swagger-codegen.git 11 | * Do not edit the class manually. 12 | */ 13 | 14 | 15 | export * from "./api"; 16 | export * from "./configuration"; 17 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | } 4 | 5 | html, body, #root { 6 | height: 100%; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | padding: 0; 12 | font-family: sans-serif; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Skydive WebUI 23 | 24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Sylvain Afchain 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | import * as React from 'react' 19 | import * as ReactDOM from 'react-dom' 20 | import 'roboto-fontface/css/roboto/roboto-fontface.css' 21 | import { SnackbarProvider } from 'notistack' 22 | import '@fortawesome/fontawesome-free/css/all.css' 23 | import { Provider, connect } from 'react-redux' 24 | import { createBrowserHistory } from 'history' 25 | import { BrowserRouter, Route, Redirect, Switch, withRouter } from 'react-router-dom' 26 | 27 | import './index.css' 28 | import { AppState, store } from './Store' 29 | import Login from './Login' 30 | import App from './App' 31 | import Logo from '../assets/Logo.png' 32 | 33 | const queryString = require('query-string') 34 | 35 | // from index.html 36 | declare var baseURL: string 37 | 38 | const history = createBrowserHistory() 39 | 40 | export const mapStateToProps = (state: AppState) => ({ 41 | session: state.session 42 | }) 43 | 44 | export const mapDispatchToProps = ({ 45 | }) 46 | 47 | const PrivateRoute = connect(mapStateToProps, mapDispatchToProps)(({ component, session, ...props }: any) => { 48 | const routeComponent = (props: any) => ( 49 | session.endpoint 50 | ? React.createElement(component, props) 51 | : 52 | ) 53 | return 54 | }) 55 | 56 | interface Props { 57 | location: any 58 | } 59 | 60 | class SkydiveApp extends React.Component { 61 | 62 | constructor(props) { 63 | super(props) 64 | } 65 | 66 | render() { 67 | const parsed = queryString.parse(this.props.location.search) 68 | return } /> 69 | } 70 | } 71 | 72 | ReactDOM.render( 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | , 83 | document.getElementById('index') 84 | ) -------------------------------------------------------------------------------- /tests/DataNormalizer.test.ts: -------------------------------------------------------------------------------- 1 | import { DataNormalizer } from '../src/StdDataNormalizer' 2 | 3 | var assert = require('assert'); 4 | describe('DataNormalizer', function () { 5 | describe('#normalize()', function () { 6 | it('should have flatten the data', function () { 7 | var data = [ 8 | { 9 | "ID": 255, 10 | "Src": "172.17.0.1" 11 | }, 12 | { 13 | "ID": 455, 14 | "Mask": "255.255.0.0" 15 | } 16 | ] 17 | 18 | var columns = ['ID', 'Src', 'Mask'] 19 | var rows = [ 20 | [255, "172.17.0.1", ""], 21 | [455, "", "255.255.0.0"] 22 | ] 23 | 24 | var dn = new DataNormalizer() 25 | var normalized = dn.normalize(data) 26 | 27 | assert.deepEqual(normalized.columns, columns) 28 | assert.deepEqual(normalized.rows, rows) 29 | }); 30 | }); 31 | }); -------------------------------------------------------------------------------- /tools/csvstoskyui/README.md: -------------------------------------------------------------------------------- 1 | # csvtoskyui 2 | 3 | ## Tool to visualize topology from CSVs Using Modern Skydive UI 4 | - - - - 5 | 6 | ![](https://github.com/skydive-project/skydive-ui/blob/master/tools/csvstoskyui/images/readme_screenshot.jpg?raw=true) 7 | 8 | ### Data sources (examples) : 9 | 1. data/example_datacenter 10 | 1. data/example_submariner 11 | 1. data/example_scale 12 | 1. data/example_multijson 13 | 14 | ### Note: this project is using [Skydive UI](https://github.com/skydive-project/skydive-ui) 15 | 16 | ### Installation 17 | * Clone this repository 18 | * Execute `./do.sh -d example_datacenter` 19 | * Browse to -------------------------------------------------------------------------------- /tools/csvstoskyui/csvstoskyui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import csv 4 | import json 5 | import re 6 | import argparse 7 | 8 | """ 9 | The content of topologyrules.conf file includes a python dictionary with the following keys: 10 | 11 | filename - The name of the file to read the csv information from 12 | example: "netbox/netbox_sites.csv" 13 | show - Boolean that enables or disables usage of this csv source 14 | example: False --> will result in skipping this csv source 15 | valuesmappingcopy - dictionary of csv values to copy from csv to json 16 | example: {"group_name": "Name"} --> Copy csv values from column 'group_name' to be called 'Name' in json 17 | valuesfilter - dictionary of csv values to filter by using regex 18 | example: "site": ".*ams01" --> Using only csv entries with site equal to regex 19 | edges - Create edges between nodes (list of edges) 20 | example: {"from":["site"], "to": {"site": ["name"]}, "RelationType": "ownership"} --> Will create 21 | an edge from every entry in csv having 'site' field that is equal to field 'name' in the csv source 22 | 'site' and set RelationType of the edge to be ownership (Note: can be list of fields to compare as needed) 23 | """ 24 | 25 | 26 | # build nodes 27 | def buildNodes(netboxtopologyfiles): 28 | jsonnodes = [] 29 | for key in netboxtopologyfiles: 30 | jsontopology = [] 31 | if not ("show" in netboxtopologyfiles[key]) or not (netboxtopologyfiles[key]["show"]): 32 | continue 33 | 34 | csv_reader = csv.DictReader(open(netboxtopologyfiles[key]["filename"])) 35 | for idx, raw in enumerate(csv_reader): 36 | jsondump = {"Metadata": raw, 37 | "ID": (key + "-" + str(idx)), 38 | "Origin": "SoftLayer"} 39 | jsondump["Metadata"].update({"Type": key}) 40 | if "valuesfilter" in netboxtopologyfiles[key]: 41 | filter_value = False 42 | for filterkey, filtervalue in netboxtopologyfiles[key]["valuesfilter"].iteritems(): 43 | try: 44 | if not re.search(filtervalue, raw[filterkey]): 45 | filter_value = True 46 | except TypeError: 47 | filter_value = True 48 | if filter_value: 49 | continue 50 | if "valuesmappingcopy" in netboxtopologyfiles[key]: 51 | for mapkey, mapvalue in netboxtopologyfiles[key]["valuesmappingcopy"].iteritems(): 52 | jsondump["Metadata"][mapvalue] = jsondump["Metadata"][mapkey] 53 | jsontopology.append(jsondump) 54 | jsonnodes += jsontopology 55 | return jsonnodes 56 | 57 | 58 | # build edges 59 | def buildEdges(netboxtopologyfiles, jsonnodes): 60 | jsonedges = [] 61 | for key in netboxtopologyfiles: 62 | jsontopology = [] 63 | if not ("show" in netboxtopologyfiles[key]) or not (netboxtopologyfiles[key]["show"]): 64 | continue 65 | 66 | if not ("edges" in netboxtopologyfiles[key]) or not (netboxtopologyfiles[key]["edges"]): 67 | continue 68 | 69 | edges = netboxtopologyfiles[key]["edges"] 70 | for idx, edge in enumerate(edges): 71 | relationType = edge["RelationType"] 72 | fromtype = key 73 | totype = edge["to"].keys()[0] 74 | 75 | fromvalues = edge["from"] 76 | tovalues = edge["to"][totype] 77 | 78 | for src in jsonnodes: 79 | if src["Metadata"]["Type"] == fromtype: 80 | for dst in jsonnodes: 81 | if dst["Metadata"]["Type"] == totype: 82 | match = True 83 | for idx, fromvalue in enumerate(fromvalues): 84 | tovalue = tovalues[idx] 85 | if tovalue not in dst["Metadata"] or fromvalue not in src["Metadata"] or \ 86 | dst["Metadata"][ 87 | tovalue] != \ 88 | src["Metadata"][fromvalue]: 89 | match = False 90 | if match: 91 | jsondump = {"Metadata": {"RelationType": relationType}, 92 | "ID": (str( 93 | "Edge-" + fromtype + "-" + src["ID"] + "-" + totype + "-" + 94 | dst["ID"] + "-" + relationType)), 95 | "Parent": dst["ID"], 96 | "Child": src["ID"], 97 | "Origin": "SoftLayer"} 98 | jsonedges.append(jsondump) 99 | return jsonedges 100 | 101 | 102 | def readconfiguration(topologyrulesfile): 103 | dict = eval(topologyrulesfile.read()) 104 | topologyrulesfile.close() 105 | return dict 106 | 107 | 108 | # main function 109 | def main(): 110 | parser = argparse.ArgumentParser() 111 | parser.add_argument("topologyrulesfile", 112 | help="Configuration file to include a python dictionary with rules to create topology", 113 | type=argparse.FileType('r')) 114 | args = parser.parse_args() 115 | 116 | topologyrules = readconfiguration(args.topologyrulesfile) 117 | jsonnodes = buildNodes(topologyrules) 118 | jsonedges = buildEdges(topologyrules, jsonnodes) 119 | skydiveuijson = [{"Nodes": jsonnodes, "Edges": jsonedges}] 120 | print json.dumps(skydiveuijson, indent=4, sort_keys=True) 121 | 122 | 123 | """ 124 | Example (minimal) for format of output json to be used by Skydive 125 | -==--=-=-==--==--=-==--=-=-==--=-=-=-=-==--= 126 | [ 127 | { 128 | "Nodes": [ 129 | { 130 | "ID": "testid" 131 | "Metadata": { 132 | "Type": "host", 133 | "Name": "test" 134 | } 135 | } 136 | ], 137 | "Edges": [ 138 | { 139 | "Parent": "testid" 140 | "Child": "testid", 141 | "ID": "testedge", 142 | "Metadata": { 143 | "RelationType": "ownership" 144 | } 145 | ] 146 | } 147 | ] 148 | """ 149 | 150 | if __name__ == '__main__': 151 | main() 152 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_datacenter/applications.csv: -------------------------------------------------------------------------------- 1 | Name 2 | Database 3 | Web 4 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_datacenter/data_centers.csv: -------------------------------------------------------------------------------- 1 | Name,Location 2 | DataCenter1,East 3 | DataCenter2,West 4 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_datacenter/dump.conf: -------------------------------------------------------------------------------- 1 | { 2 | "data_center": {"filename": "data/example_datacenter/data_centers.csv", 3 | "show": True}, 4 | "host": {"filename": "data/example_datacenter/hosts.csv", 5 | "show": True, 6 | "edges": [{"from": ["DataCenter"], "to": {"data_center": ["Name"]}, "RelationType": "ownership"}]}, 7 | "vm": {"filename": "data/example_datacenter/vms.csv", 8 | "show": True, 9 | "edges": [{"from": ["Host"], "to": {"host": ["Name"]}, "RelationType": "ownership"}, 10 | {"from": ["Application"], "to": {"application": ["Name"]}, "RelationType": "application"}]}, 11 | "application": {"filename": "data/example_datacenter/applications.csv", 12 | "show": True} 13 | } -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_datacenter/hosts.csv: -------------------------------------------------------------------------------- 1 | Name,CPUs,Memory,DataCenter 2 | Host1,4,64,DataCenter1 3 | Host2,4,64,DataCenter1 4 | Host3,4,128,DataCenter2 5 | Host4,16,64,DataCenter2 6 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_datacenter/vms.csv: -------------------------------------------------------------------------------- 1 | Name,Host,Application 2 | VM1,Host1,Web 3 | VM2,Host1,Database 4 | VM3,Host2,Database 5 | VM4,Host2,Database 6 | VM5,Host3,Database 7 | VM6,Host4,Web 8 | VM7,Host4,Database 9 | 10 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_multijson/DataCenter1.conf: -------------------------------------------------------------------------------- 1 | { 2 | "data_center": {"filename": "data/example_multijson/data_centers.csv", 3 | "valuesfilter": {"Name": "DataCenter1"}, 4 | "show": True}, 5 | "host": {"filename": "data/example_multijson/hosts.csv", 6 | "show": True, 7 | "valuesfilter": {"DataCenter": "DataCenter1"}, 8 | "edges": [{"from": ["DataCenter"], "to": {"data_center": ["Name"]}, "RelationType": "ownership"}]}, 9 | "vm": {"filename": "data/example_multijson/vms.csv", 10 | "show": True, 11 | "valuesfilter": {"Host": "Host1|Host2"}, 12 | "edges": [{"from": ["Host"], "to": {"host": ["Name"]}, "RelationType": "ownership"}, 13 | {"from": ["Application"], "to": {"application": ["Name"]}, "RelationType": "application"}]}, 14 | "application": {"filename": "data/example_multijson/applications.csv", 15 | "show": True} 16 | } -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_multijson/DataCenter2.conf: -------------------------------------------------------------------------------- 1 | { 2 | "data_center": {"filename": "data/example_multijson/data_centers.csv", 3 | "valuesfilter": {"Name": "DataCenter2"}, 4 | "show": True}, 5 | "host": {"filename": "data/example_multijson/hosts.csv", 6 | "show": True, 7 | "valuesfilter": {"DataCenter": "DataCenter2"}, 8 | "edges": [{"from": ["DataCenter"], "to": {"data_center": ["Name"]}, "RelationType": "ownership"}]}, 9 | "vm": {"filename": "data/example_multijson/vms.csv", 10 | "show": True, 11 | "valuesfilter": {"Host": "Host3|Host4"}, 12 | "edges": [{"from": ["Host"], "to": {"host": ["Name"]}, "RelationType": "ownership"}, 13 | {"from": ["Application"], "to": {"application": ["Name"]}, "RelationType": "application"}]}, 14 | "application": {"filename": "data/example_multijson/applications.csv", 15 | "show": True} 16 | } -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_multijson/applications.csv: -------------------------------------------------------------------------------- 1 | Name 2 | Database 3 | Web 4 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_multijson/data_centers.csv: -------------------------------------------------------------------------------- 1 | Name,Location,Expandable 2 | DataCenter1,East,True 3 | DataCenter2,West,True 4 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_multijson/dump.conf: -------------------------------------------------------------------------------- 1 | { 2 | "data_center": {"filename": "data/example_multijson/data_centers.csv", 3 | "show": True} 4 | } -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_multijson/hosts.csv: -------------------------------------------------------------------------------- 1 | Name,CPUs,Memory,DataCenter 2 | Host1,4,64,DataCenter1 3 | Host2,4,64,DataCenter1 4 | Host3,4,128,DataCenter2 5 | Host4,16,64,DataCenter2 6 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_multijson/vms.csv: -------------------------------------------------------------------------------- 1 | Name,Host,Application 2 | VM1,Host1,Web 3 | VM2,Host1,Database 4 | VM3,Host2,Database 5 | VM4,Host2,Database 6 | VM5,Host3,Database 7 | VM6,Host4,Web 8 | VM7,Host4,Database 9 | 10 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_scale/applications.csv: -------------------------------------------------------------------------------- 1 | Name,Tier 2 | Database,3 3 | Logic,2 4 | Web,1 5 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_scale/config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | "nodeAttrs": function (node) { 3 | console.log(node.data) 4 | var name = node.data.Name 5 | if (name.length > 24) { 6 | name = node.data.Name.substring(0, 24) + "." 7 | } 8 | 9 | var attrs = { classes: [node.data.Type], name: name, icon: "\uf192", iconClass: '', weight: 0 } 10 | 11 | if (node.data.OfPort) { 12 | attrs.weight = 15 13 | } 14 | 15 | if (node.data.Manager === "k8s") { 16 | attrs.icon = "/assets/icons/k8s.png" 17 | attrs.weight = 1 18 | } 19 | 20 | switch (node.data.Type) { 21 | case "data_center": 22 | attrs.icon = "\uf109" 23 | attrs.weight = 2 24 | break 25 | case "host": 26 | attrs.icon = "\uf109" 27 | attrs.weight = 3 28 | break 29 | case "vm": 30 | attrs.icon = "\uf109" 31 | attrs.weight = 4 32 | break 33 | case "application": 34 | attrs.icon = "\uf109" 35 | attrs.weight = 5 36 | break 37 | case "switch": 38 | attrs.icon = "\uf6ff" 39 | break 40 | case "bridge": 41 | case "ovsbridge": 42 | attrs.icon = "\uf6ff" 43 | attrs.weight = 14 44 | break 45 | case "erspan": 46 | attrs.icon = "\uf1e0" 47 | break 48 | case "geneve": 49 | case "vxlan": 50 | case "gre": 51 | case "gretap": 52 | attrs.icon = "\uf55b" 53 | break 54 | case "device": 55 | case "internal": 56 | case "interface": 57 | case "tun": 58 | case "tap": 59 | attrs.icon = "\uf796" 60 | attrs.weight = 17 61 | break 62 | case "veth": 63 | attrs.icon = "\uf4d7" 64 | attrs.weight = 17 65 | break 66 | case "switchport": 67 | attrs.icon = "\uf0e8" 68 | break 69 | case "patch": 70 | case "port": 71 | case "ovsport": 72 | attrs.icon = "\uf0e8" 73 | attrs.weight = 15 74 | break 75 | case "netns": 76 | attrs.icon = "\uf24d" 77 | attrs.weight = 18 78 | break 79 | case "libvirt": 80 | attrs.icon = "\uf109" 81 | attrs.weight = 19 82 | break 83 | case "cluster": 84 | attrs.icon = "/assets/icons/cluster.png" 85 | break 86 | case "configmap": 87 | attrs.icon = "/assets/icons/configmap.png" 88 | break 89 | case "container": 90 | attrs.icon = "/assets/icons/container.png" 91 | break 92 | case "cronjob": 93 | attrs.icon = "/assets/icons/cronjob.png" 94 | break 95 | case "daemonset": 96 | attrs.icon = "/assets/icons/daemonset.png" 97 | break 98 | case "deployment": 99 | attrs.icon = "/assets/icons/deployment.png" 100 | break 101 | case "endpoints": 102 | attrs.icon = "/assets/icons/endpoints.png" 103 | break 104 | case "ingress": 105 | attrs.icon = "/assets/icons/ingress.png" 106 | break 107 | case "job": 108 | attrs.icon = "/assets/icons/job.png" 109 | break 110 | case "node": 111 | attrs.icon = "\uf109" 112 | break 113 | case "persistentvolume": 114 | attrs.icon = "/assets/icons/persistentvolume.png" 115 | break 116 | case "persistentvolumeclaim": 117 | attrs.icon = "/assets/icons/persistentvolumeclaim.png" 118 | break 119 | case "pod": 120 | attrs.icon = "/assets/icons/pod.png" 121 | break 122 | case "networkpolicy": 123 | attrs.icon = "/assets/icons/networkpolicy.png" 124 | break 125 | case "namespace": 126 | attrs.icon = "\uf24d" 127 | break 128 | case "replicaset": 129 | attrs.icon = "/assets/icons/replicaset.png" 130 | break 131 | case "replicationcontroller": 132 | attrs.icon = "/assets/icons/replicationcontroller.png" 133 | break 134 | case "secret": 135 | attrs.icon = "/assets/icons/secret.png" 136 | break 137 | case "service": 138 | attrs.icon = "/assets/icons/service.png" 139 | break 140 | case "statefulset": 141 | attrs.icon = "/assets/icons/statefulset.png" 142 | break 143 | case "storageclass": 144 | attrs.icon = "/assets/icons/storageclass.png" 145 | break 146 | default: 147 | attrs.icon = "\uf192" 148 | break 149 | } 150 | 151 | if (node.data.Manager === "docker") { 152 | attrs.icon = "\uf395" 153 | attrs.iconClass = "font-brands" 154 | } 155 | 156 | if (node.data.IPV4 && node.data.IPV4.length) { 157 | attrs.weight = 13 158 | } 159 | 160 | if (node.data.Driver && ["tap", "veth", "tun", "openvswitch"].indexOf(node.data.Driver) < 0) { 161 | attrs.weight = 13 162 | } 163 | 164 | if (node.data.Probe === "fabric") { 165 | attrs.weight = 10 166 | } 167 | 168 | return attrs 169 | }, 170 | "nodeTags": function (data) { 171 | switch (data.Type) { 172 | case "VLAN": 173 | return ["logical"] 174 | case "tenant": 175 | return ["logical"] 176 | default: 177 | return ["physical"] 178 | } 179 | }, 180 | "defaultNodeTag": "physical", 181 | "nodeTabTitle": function (node) { 182 | return node.data.Name.substring(0, 8) 183 | }, 184 | groupSize: 10, 185 | "groupBy": function (node) { 186 | return node.data.Type && node.data.Type !== "host" ? node.data.Type : null 187 | }, 188 | "weightTitles": { 189 | 0: "Not classified", 190 | 1: "Kubernetes", 191 | 2: "data_center", 192 | 3: "host", 193 | 4: "vm", 194 | 5: "application", 195 | 10: "Fabric", 196 | 13: "Physical", 197 | 14: "Bridges", 198 | 15: "Ports", 199 | 17: "Virtual", 200 | 18: "Namespaces", 201 | 19: "VMs" 202 | }, 203 | "suggestions": [ 204 | "data.IPV4", 205 | "data.MAC", 206 | "data.Name" 207 | ], 208 | "nodeDataFields": [ 209 | { 210 | field: "", 211 | title: "General", 212 | expanded: true, 213 | icon: "\uf05a", 214 | sortKeys: function (data) { 215 | return ['Name', 'Type', 'MAC', 'Driver', 'State'] 216 | }, 217 | filterKeys: function (data) { 218 | switch (data.Type) { 219 | case "host": 220 | return ['Name'] 221 | default: 222 | return ['Name', 'Type', 'MAC', 'Driver', 'State'] 223 | } 224 | } 225 | }, 226 | { 227 | field: "Sockets", 228 | expanded: false, 229 | icon: "\uf1e6" 230 | }, 231 | { 232 | field: "Captures", 233 | expanded: false, 234 | icon: "\uf51f", 235 | normalizer: function (data) { 236 | for (let capture of data) { 237 | capture.ID = capture.ID.split('-')[0] 238 | } 239 | return data 240 | } 241 | }, 242 | { 243 | field: "Injections", 244 | expanded: false, 245 | icon: "\uf48e" 246 | }, 247 | { 248 | field: "Docker", 249 | expanded: false, 250 | icon: "\uf395", 251 | iconClass: "font-brands" 252 | }, 253 | { 254 | field: "IPV4", 255 | expanded: true, 256 | icon: "\uf1fa" 257 | }, 258 | { 259 | field: "IPV6", 260 | expanded: true, 261 | icon: "\uf1fa" 262 | }, 263 | { 264 | field: "LastUpdateMetric", 265 | title: "Last metrics", 266 | expanded: false, 267 | icon: "\uf201", 268 | normalizer: function (data) { 269 | return { 270 | RxPackets: data.RxPackets ? data.RxPackets.toLocaleString() : 0, 271 | RxBytes: data.RxBytes ? prettyBytes(data.RxBytes) : 0, 272 | TxPackets: data.TxPackets ? data.TxPackets.toLocaleString() : 0, 273 | TxBytes: data.TxPackets ? prettyBytes(data.TxBytes) : 0, 274 | Start: data.Start ? new Date(data.Start).toLocaleString() : 0, 275 | Last: data.Last ? new Date(data.Last).toLocaleString() : 0 276 | } 277 | }, 278 | graph: function (data) { 279 | return { 280 | type: "LineChart", 281 | data: [ 282 | [ 283 | { type: "datetime", label: "time" }, 284 | "RxBytes", 285 | "TxBytes" 286 | ], 287 | [new Date(data.Last || 0), data.RxBytes || 0, data.TxBytes || 0] 288 | ] 289 | } 290 | } 291 | }, 292 | { 293 | field: "Metric", 294 | title: "Total metrics", 295 | expanded: false, 296 | icon: "\uf201", 297 | normalizer: function (data) { 298 | return { 299 | RxPackets: data.RxPackets ? data.RxPackets.toLocaleString() : 0, 300 | RxBytes: data.RxBytes ? prettyBytes(data.RxBytes) : 0, 301 | TxPackets: data.TxPackets ? data.TxPackets.toLocaleString() : 0, 302 | TxBytes: data.TxPackets ? prettyBytes(data.TxBytes) : 0, 303 | Last: data.Last ? new Date(data.Last).toLocaleString() : 0 304 | } 305 | } 306 | }, 307 | { 308 | field: "Features", 309 | expanded: false, 310 | icon: "\uf022" 311 | }, 312 | { 313 | field: "FDB", 314 | expanded: false, 315 | icon: "\uf0ce" 316 | }, 317 | { 318 | field: "Neighbors", 319 | expanded: false, 320 | icon: "\uf0ce" 321 | }, 322 | { 323 | field: "RoutingTables", 324 | title: "Routing tables", 325 | expanded: false, 326 | icon: "\uf0ce", 327 | normalizer: function (data) { 328 | var rows = [] 329 | for (let table of data) { 330 | if (!table.Routes) { 331 | continue 332 | } 333 | for (let route of table.Routes) { 334 | if (!route.NextHops) { 335 | continue 336 | } 337 | for (let nh of route.NextHops) { 338 | rows.push({ 339 | ID: table.ID, 340 | Src: table.Src, 341 | Protocol: route["Protocol"], 342 | Prefix: route["Prefix"], 343 | Priority: nh["Priority"], 344 | IP: nh["IP"], 345 | IfIndex: nh["IfIndex"] 346 | }) 347 | } 348 | } 349 | } 350 | 351 | return rows 352 | } 353 | } 354 | ], 355 | "linkAttrs": function (link) { 356 | var attrs = { classes: [link.data.RelationType], icon: "\uf362", directed: false } 357 | 358 | if (link.data.Directed) { 359 | attrs.directed = true 360 | } 361 | 362 | return attrs 363 | }, 364 | "linkTabTitle": function (link) { 365 | var src = link.source.data.Name 366 | var dst = link.target.data.Name 367 | if (src && dst) { 368 | return src.substring(0, 8) + " / " + dst.substring(0, 8) 369 | } 370 | return link.id.split("-")[0] 371 | }, 372 | "linkDataFields": [ 373 | { 374 | field: "", 375 | title: "General", 376 | expanded: true, 377 | icon: "\uf05a", 378 | }, 379 | { 380 | field: "NSM", 381 | title: "Network Service Mesh", 382 | expanded: true, 383 | icon: "\uf542", 384 | }, 385 | { 386 | field: "NSM.Source", 387 | title: "Source", 388 | expanded: false, 389 | icon: "\uf018", 390 | }, 391 | { 392 | field: "NSM.Via", 393 | title: "Via", 394 | expanded: false, 395 | icon: "\uf018", 396 | }, 397 | { 398 | field: "NSM.Destination", 399 | title: "Destination", 400 | expanded: false, 401 | icon: "\uf018", 402 | }, 403 | ] 404 | } -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_scale/data_centers.csv: -------------------------------------------------------------------------------- 1 | Name,Location 2 | DataCenter1,Lyon 3 | DataCenter2,Willow Run 4 | DataCenter3,Conyersville 5 | DataCenter4,Mount Baker 6 | DataCenter5,Farmington Lake 7 | DataCenter6,Martins Corner 8 | DataCenter7,Pickerel Narrows 9 | DataCenter8,Willaha 10 | DataCenter9,Center 11 | DataCenter10,Spring City 12 | DataCenter11,Mittenlane 13 | DataCenter12,East Waterford 14 | DataCenter13,Coltman 15 | DataCenter14,Scottsville 16 | DataCenter15,Hebron 17 | DataCenter16,Longview 18 | DataCenter17,Emerson 19 | DataCenter18,North Knoxville 20 | DataCenter19,Momford Landing 21 | DataCenter20,Ipswich 22 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_scale/dump.conf: -------------------------------------------------------------------------------- 1 | { 2 | "data_center": {"filename": "data/example_scale/data_centers.csv", 3 | "show": True}, 4 | "host": {"filename": "data/example_scale/hosts.csv", 5 | "show": True, 6 | "edges": [{"from": ["DataCenter"], "to": {"data_center": ["Name"]}, "RelationType": "ownership"}]}, 7 | "vm": {"filename": "data/example_scale/vms.csv", 8 | "show": True, 9 | "edges": [{"from": ["Host"], "to": {"host": ["Name"]}, "RelationType": "ownership"}, 10 | {"from": ["Application"], "to": {"application": ["Name"]}, "RelationType": "application"}]}, 11 | "application": {"filename": "data/example_scale/applications.csv", 12 | "show": True} 13 | } -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_scale/hosts.csv: -------------------------------------------------------------------------------- 1 | Name,CPUs,Memory,DataCenter 2 | Host1,8,128,DataCenter16 3 | Host2,4,128,DataCenter16 4 | Host3,8,128,DataCenter2 5 | Host4,8,64,DataCenter15 6 | Host5,16,128,DataCenter1 7 | Host6,4,64,DataCenter7 8 | Host7,4,128,DataCenter17 9 | Host8,16,64,DataCenter19 10 | Host9,4,128,DataCenter10 11 | Host10,16,128,DataCenter8 12 | Host11,16,32,DataCenter12 13 | Host12,4,128,DataCenter4 14 | Host13,16,32,DataCenter4 15 | Host14,16,64,DataCenter4 16 | Host15,4,128,DataCenter2 17 | Host16,16,32,DataCenter12 18 | Host17,4,32,DataCenter10 19 | Host18,8,128,DataCenter7 20 | Host19,16,128,DataCenter3 21 | Host20,8,64,DataCenter13 22 | Host21,4,128,DataCenter8 23 | Host22,16,64,DataCenter17 24 | Host23,4,128,DataCenter14 25 | Host24,8,128,DataCenter7 26 | Host25,16,128,DataCenter14 27 | Host26,4,32,DataCenter10 28 | Host27,16,64,DataCenter18 29 | Host28,8,32,DataCenter7 30 | Host29,4,32,DataCenter6 31 | Host30,4,32,DataCenter20 32 | Host31,8,32,DataCenter14 33 | Host32,16,64,DataCenter17 34 | Host33,8,128,DataCenter1 35 | Host34,16,64,DataCenter3 36 | Host35,4,64,DataCenter11 37 | Host36,16,32,DataCenter11 38 | Host37,8,128,DataCenter6 39 | Host38,4,32,DataCenter4 40 | Host39,4,64,DataCenter8 41 | Host40,16,64,DataCenter17 42 | Host41,16,128,DataCenter3 43 | Host42,16,32,DataCenter2 44 | Host43,4,128,DataCenter7 45 | Host44,8,32,DataCenter6 46 | Host45,8,128,DataCenter9 47 | Host46,16,32,DataCenter3 48 | Host47,4,32,DataCenter7 49 | Host48,8,32,DataCenter5 50 | Host49,8,32,DataCenter7 51 | Host50,8,32,DataCenter18 52 | Host51,4,128,DataCenter9 53 | Host52,16,128,DataCenter17 54 | Host53,8,128,DataCenter7 55 | Host54,16,32,DataCenter20 56 | Host55,16,32,DataCenter14 57 | Host56,8,32,DataCenter3 58 | Host57,4,128,DataCenter3 59 | Host58,16,64,DataCenter17 60 | Host59,8,128,DataCenter10 61 | Host60,8,64,DataCenter13 62 | Host61,16,64,DataCenter11 63 | Host62,8,128,DataCenter5 64 | Host63,16,64,DataCenter2 65 | Host64,16,32,DataCenter4 66 | Host65,8,64,DataCenter5 67 | Host66,4,128,DataCenter15 68 | Host67,8,64,DataCenter3 69 | Host68,16,64,DataCenter14 70 | Host69,8,128,DataCenter20 71 | Host70,4,32,DataCenter20 72 | Host71,16,64,DataCenter4 73 | Host72,4,32,DataCenter1 74 | Host73,16,64,DataCenter13 75 | Host74,16,128,DataCenter16 76 | Host75,16,32,DataCenter17 77 | Host76,4,64,DataCenter3 78 | Host77,16,128,DataCenter16 79 | Host78,16,32,DataCenter11 80 | Host79,16,32,DataCenter16 81 | Host80,16,64,DataCenter10 82 | Host81,8,32,DataCenter12 83 | Host82,8,32,DataCenter6 84 | Host83,16,128,DataCenter5 85 | Host84,4,128,DataCenter12 86 | Host85,16,64,DataCenter4 87 | Host86,8,64,DataCenter11 88 | Host87,4,32,DataCenter18 89 | Host88,16,32,DataCenter5 90 | Host89,8,128,DataCenter9 91 | Host90,8,64,DataCenter17 92 | Host91,8,64,DataCenter17 93 | Host92,8,32,DataCenter20 94 | Host93,8,32,DataCenter15 95 | Host94,8,32,DataCenter17 96 | Host95,4,128,DataCenter11 97 | Host96,16,128,DataCenter9 98 | Host97,8,64,DataCenter15 99 | Host98,4,32,DataCenter5 100 | Host99,16,128,DataCenter18 101 | Host100,16,32,DataCenter13 102 | Host101,8,128,DataCenter9 103 | Host102,16,32,DataCenter17 104 | Host103,16,32,DataCenter18 105 | Host104,4,32,DataCenter2 106 | Host105,16,64,DataCenter7 107 | Host106,16,128,DataCenter19 108 | Host107,16,32,DataCenter19 109 | Host108,16,128,DataCenter18 110 | Host109,8,64,DataCenter7 111 | Host110,4,32,DataCenter17 112 | Host111,16,32,DataCenter19 113 | Host112,4,128,DataCenter12 114 | Host113,4,32,DataCenter3 115 | Host114,16,128,DataCenter19 116 | Host115,4,32,DataCenter14 117 | Host116,16,128,DataCenter3 118 | Host117,4,32,DataCenter8 119 | Host118,16,32,DataCenter7 120 | Host119,4,128,DataCenter20 121 | Host120,16,32,DataCenter15 122 | Host121,8,128,DataCenter14 123 | Host122,4,128,DataCenter9 124 | Host123,16,64,DataCenter19 125 | Host124,4,32,DataCenter16 126 | Host125,8,64,DataCenter6 127 | Host126,16,128,DataCenter18 128 | Host127,16,32,DataCenter5 129 | Host128,4,64,DataCenter10 130 | Host129,8,32,DataCenter6 131 | Host130,4,32,DataCenter3 132 | Host131,4,64,DataCenter13 133 | Host132,16,128,DataCenter15 134 | Host133,16,32,DataCenter2 135 | Host134,16,128,DataCenter2 136 | Host135,8,128,DataCenter9 137 | Host136,8,64,DataCenter13 138 | Host137,8,64,DataCenter6 139 | Host138,4,32,DataCenter19 140 | Host139,8,64,DataCenter19 141 | Host140,16,32,DataCenter20 142 | Host141,4,64,DataCenter17 143 | Host142,4,32,DataCenter10 144 | Host143,8,128,DataCenter7 145 | Host144,8,64,DataCenter5 146 | Host145,4,128,DataCenter4 147 | Host146,4,128,DataCenter3 148 | Host147,4,64,DataCenter15 149 | Host148,16,128,DataCenter10 150 | Host149,16,128,DataCenter14 151 | Host150,16,128,DataCenter20 152 | Host151,16,128,DataCenter5 153 | Host152,8,32,DataCenter10 154 | Host153,8,64,DataCenter9 155 | Host154,16,64,DataCenter18 156 | Host155,4,32,DataCenter17 157 | Host156,8,128,DataCenter16 158 | Host157,16,128,DataCenter19 159 | Host158,8,128,DataCenter1 160 | Host159,4,128,DataCenter2 161 | Host160,8,32,DataCenter4 162 | Host161,8,64,DataCenter10 163 | Host162,8,128,DataCenter5 164 | Host163,16,32,DataCenter9 165 | Host164,16,128,DataCenter19 166 | Host165,4,32,DataCenter13 167 | Host166,8,128,DataCenter11 168 | Host167,8,32,DataCenter9 169 | Host168,8,64,DataCenter17 170 | Host169,4,32,DataCenter10 171 | Host170,4,32,DataCenter1 172 | Host171,4,32,DataCenter4 173 | Host172,16,64,DataCenter9 174 | Host173,4,128,DataCenter4 175 | Host174,8,128,DataCenter3 176 | Host175,4,32,DataCenter6 177 | Host176,4,32,DataCenter16 178 | Host177,4,128,DataCenter17 179 | Host178,16,64,DataCenter2 180 | Host179,4,32,DataCenter5 181 | Host180,16,128,DataCenter17 182 | Host181,4,32,DataCenter7 183 | Host182,16,128,DataCenter15 184 | Host183,4,128,DataCenter2 185 | Host184,16,64,DataCenter16 186 | Host185,4,128,DataCenter16 187 | Host186,8,128,DataCenter5 188 | Host187,4,64,DataCenter14 189 | Host188,16,128,DataCenter10 190 | Host189,8,32,DataCenter6 191 | Host190,4,32,DataCenter17 192 | Host191,8,128,DataCenter19 193 | Host192,8,64,DataCenter3 194 | Host193,8,32,DataCenter10 195 | Host194,4,64,DataCenter11 196 | Host195,8,64,DataCenter11 197 | Host196,8,32,DataCenter2 198 | Host197,16,128,DataCenter9 199 | Host198,16,32,DataCenter9 200 | Host199,4,128,DataCenter12 201 | Host200,16,32,DataCenter9 202 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_submariner/brokers.csv: -------------------------------------------------------------------------------- 1 | Name 2 | Broker1 3 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_submariner/clusters.csv: -------------------------------------------------------------------------------- 1 | Name,Broker,Location,PodCidr,ServiceCidr 2 | Cluster1,Broker1,East,10.95.0.0/16,10.245.0.0/16 3 | Cluster2,Broker1,West,10.96.0.0/16,10.246.0.0/16 4 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_submariner/dump.conf: -------------------------------------------------------------------------------- 1 | { 2 | "Broker": {"filename": "data/example_submariner/brokers.csv", 3 | "show": True}, 4 | "Cluster": {"filename": "data/example_submariner/clusters.csv", 5 | "show": True, 6 | "edges": [{"from": ["Broker"], "to": {"Broker": ["Name"]}, "RelationType": "ownership"}]}, 7 | "Node": {"filename": "data/example_submariner/nodes.csv", 8 | "show": True, 9 | "edges": [{"from": ["Cluster"], "to": {"Cluster": ["Name"]}, "RelationType": "ownership"}]}, 10 | "Pod": {"filename": "data/example_submariner/pods.csv", 11 | "show": True, 12 | "edges": [{"from": ["Node"], "to": {"Node": ["Name"]}, "RelationType": "ownership"}, 13 | {"from": ["ipsec"], "to": {"Pod": ["ipsec"]}, "RelationType": "ipsec"}, 14 | {"from": ["vxlan"], "to": {"Pod": ["vxlan"]}, "RelationType": "vxlan"},]} 15 | } -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_submariner/nodes.csv: -------------------------------------------------------------------------------- 1 | Name,Cluster 2 | Node11,Cluster1 3 | Node21,Cluster2 4 | Node22,Cluster2 5 | 6 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_submariner/pods.csv: -------------------------------------------------------------------------------- 1 | Name,Node,Rule,ipsec,vxlan 2 | 0_nginx,Node11,nginx,0,0 3 | 1_routeagent,Node11,routeagent,1,vxlan1 4 | 2_gateway11,Node11,gateway,ipsec_VPN1,2 5 | 3_gateway21,Node21,gateway,ipsec_VPN1,3 6 | 4_routeagent,Node21,routeagent,4,vxlan2 7 | 5_routeagent,Node22,routeagent,5,vxlan2 8 | 6_Busybox,Node22,usybox,6,6 9 | -------------------------------------------------------------------------------- /tools/csvstoskyui/data/example_submariner/topology.css: -------------------------------------------------------------------------------- 1 | .host .node-hexagon { 2 | fill: #114B5F; 3 | } 4 | 5 | .netns .node-hexagon { 6 | fill: #C6DABF; 7 | } 8 | 9 | .bridge .node-hexagon, .ovsbridge .node-hexagon, .port .node-hexagon, .ovsport .node-hexagon { 10 | fill: #36b791; 11 | } 12 | 13 | .interface .node-hexagon, .device .node-hexagon, .tun .node-hexagon, .tap .node-hexagon, .veth .node-hexagon { 14 | fill: #88D498; 15 | } 16 | 17 | .down .node-hexagon { 18 | fill: rgb(163, 0, 0); 19 | } 20 | 21 | .host .node-icon { 22 | fill: #eee; 23 | } 24 | 25 | .netns .node-icon { 26 | fill: #444; 27 | } 28 | 29 | .bridge .node-icon, .ovsbridge .node-icon, .port .node-icon, .ovsport .node-icon { 30 | fill: #eee; 31 | } 32 | 33 | .interface .node-icon, .device .node-icon, .tun .node-icon, .tap .node-icon, .veth .node-icon { 34 | fill: #444; 35 | } 36 | 37 | .down .node-icon { 38 | fill: #eee; 39 | } 40 | 41 | .traffic { 42 | stroke-dasharray: 5; 43 | animation: traffic 3s linear; 44 | animation-iteration-count: infinite; 45 | } 46 | 47 | .ipsec_traffic { 48 | stroke: #00FF11; 49 | stroke-width: 5px; 50 | markerWidth: 3px; 51 | markerHeight: 3px; 52 | } 53 | 54 | .vxlan_traffic { 55 | stroke: #00FFFF; 56 | } 57 | 58 | @keyframes traffic { 59 | to { 60 | stroke-dashoffset: -100; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tools/csvstoskyui/do.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dir="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" 4 | data_dir="example_datacenter" 5 | usage="$(basename "$0") [-h] [-d] -- convert csvs from example folder to json and start skydive ui from container 6 | 7 | where: 8 | -h show this help text 9 | -d example direcory 10 | 11 | Avaliable example direcories (for -d): 12 | 13 | $(dir -1 data) 14 | 15 | Example: 16 | 17 | ./$(basename "$0") -d $data_dir 18 | " 19 | 20 | if [ "$1" == "-d" ]; then 21 | data_dir=$2 22 | else 23 | echo "$usage" >&2 24 | exit 0 25 | fi 26 | 27 | echo Using data dir : "$dir"/data/$data_dir/ 28 | 29 | unset mountconf_to_docker 30 | destjson_dir="out" 31 | rm -r $destjson_dir 32 | mkdir $destjson_dir 33 | for rules_filename in data/$data_dir/*.conf; do 34 | echo Using rules file: $rules_filename 35 | destjsonfile=$(basename "$rules_filename" .conf).json 36 | ./csvstoskyui.py $rules_filename > $destjson_dir/$destjsonfile 37 | mountconf_to_docker=$mountconf_to_docker" -v "$dir"/"$destjson_dir"/"$destjsonfile":/usr/src/skydive-ui/assets/"$destjsonfile 38 | done 39 | 40 | if [[ -f "$dir"/data/$data_dir/topology.css ]]; then 41 | mountconf_to_docker=$mountconf_to_docker" -v "$dir"/data/"$data_dir"/topology.css:/usr/src/skydive-ui/assets/topology.css" 42 | fi 43 | 44 | if [[ -f "$dir"/data/$data_dir/config.js ]]; then 45 | mountconf_to_docker=$mountconf_to_docker" -v "$dir"/data/"$data_dir"/config.js:/usr/src/skydive-ui/assets/config.js" 46 | fi 47 | 48 | if [[ -f "$dir"/data/$data_dir/Config.ts ]]; then 49 | mountconf_to_docker=$mountconf_to_docker" -v "$dir"/data/"$data_dir"/Config.ts:/usr/src/skydive-ui/src/Config.ts" 50 | fi 51 | 52 | echo Starting skydive ui conainer 53 | docker run -p 8080:8080 $mountconf_to_docker skydive/skydive-ui 54 | -------------------------------------------------------------------------------- /tools/csvstoskyui/images/readme_screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skydive-project/skydive-ui/1f732f4e5ab30ed40eb7db8a589ec2bd8df2c2bc/tools/csvstoskyui/images/readme_screenshot.jpg -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", // path to output directory 4 | "sourceMap": true, // allow sourcemap support 5 | "strictNullChecks": true, // enable strict null checks as a best practice 6 | "module": "es6", // specify module code generation 7 | "jsx": "react", // use typescript to transpile jsx to js 8 | "target": "es6", // specify ECMAScript target version 9 | "allowJs": true, // allow a partial TypeScript and JavaScript codebase 10 | "moduleResolution": "node", 11 | "lib": [ 12 | "es6", 13 | "dom", 14 | "es2017" 15 | ] 16 | }, 17 | "include": [ 18 | "./src/", 19 | "./tests/" 20 | ] 21 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 2 | const CopyWebPackPlugin = require('copy-webpack-plugin'); 3 | 4 | var path = require('path'); 5 | 6 | const htmlPlugin = new HtmlWebPackPlugin({ 7 | template: "./src/index.html", 8 | filename: "./index.html" 9 | }); 10 | 11 | module.exports = { 12 | entry: './src/index.tsx', 13 | output: { 14 | filename: './dist/bundle.js' 15 | }, 16 | resolve: { 17 | extensions: [".ts", ".tsx", ".js", ".jsx"] 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(t|j)sx?$/, 23 | use: { 24 | loader: 'awesome-typescript-loader' 25 | }, 26 | exclude: /node_modules/ 27 | }, 28 | { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }, 29 | { 30 | test: /\.css$/, 31 | use: ["style-loader", "css-loader"] 32 | }, 33 | { 34 | test: /\.(gif|png|jpe?g|svg)$/i, 35 | use: [ 36 | { 37 | loader: 'url-loader', 38 | options: { 39 | } 40 | } 41 | ], 42 | }, 43 | { 44 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, 45 | use: [ 46 | { 47 | loader: 'file-loader', 48 | options: { 49 | name: '[name].[ext]', 50 | outputPath: 'fonts/' 51 | } 52 | } 53 | ] 54 | }, 55 | ] 56 | }, 57 | 58 | devServer: { 59 | historyApiFallback: true, 60 | contentBase: path.join(__dirname, 'dist'), 61 | compress: true, 62 | port: 8080 63 | }, 64 | 65 | devtool: "source-map", 66 | 67 | plugins: [ 68 | htmlPlugin, 69 | new CopyWebPackPlugin({ 70 | patterns: [ 71 | { from: 'assets', to: 'assets' } 72 | ] 73 | }) 74 | ] 75 | } 76 | --------------------------------------------------------------------------------