├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── nginx └── ui.conf ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.js ├── components │ ├── selected-user-card-component.js │ └── user-card-component.js ├── configuration.js ├── duke.png ├── gopher.png ├── index.js ├── pages │ └── main-page.js ├── reportWebVitals.js └── unoffical_kt.png ├── ui.gif └── ui.png /.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | .git 4 | *.md 5 | .gitignore -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | package-lock.json 6 | 7 | .pnp 8 | .pnp.js 9 | 10 | # testing 11 | coverage 12 | 13 | # production 14 | build/ 15 | build_image.bat 16 | build_image_ec2.bat 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | .env 30 | .env.local 31 | 32 | 33 | .eslintcache 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 AS builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN npm install && npm run-script build 5 | 6 | FROM nginx:alpine 7 | WORKDIR /etc/nginx/conf.d 8 | RUN rm -f default.conf 9 | COPY ./nginx /etc/nginx/conf.d 10 | WORKDIR /usr/share/nginx/html 11 | RUN rm -rf ./* 12 | COPY --from=builder /app/build . 13 | ENTRYPOINT ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 uid4oe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microservices gRPC UI 2 | 3 | [Deployed in EC2, Check it out!](http://ec2-18-192-156-217.eu-central-1.compute.amazonaws.com/) 4 | 5 | ![](./ui.gif) 6 | 7 | 8 | Generic client app for performing CRUD operations through microservices at 9 | 10 | - [Microservices Go gRPC](https://github.com/uid4oe/microservices-go-grpc/) 11 | - [Microservices Kotlin gRPC](https://github.com/uid4oe/microservices-kotlin-grpc/) 12 | - [Microservices Java gRPC](https://github.com/uid4oe/microservices-java-grpc/) 13 | 14 | ## Installation 15 | ```bash 16 | npm install 17 | npm start 18 | ``` 19 | Make sure at least one set of services in the above repos is started beforehand 20 | 21 | ## Routes 22 | ```bash 23 | # [Microservices Go gRPC] 24 | http://localhost:3000/go 25 | # [Microservices Kotlin gRPC] 26 | http://localhost:3000/kotlin 27 | # [Microservices Java gRPC] 28 | http://localhost:3000/java 29 | ``` 30 | 31 | 32 | ## Contributing 33 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /nginx/ui.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name uid4oe.dev www.uid4oe.dev; 4 | 5 | listen 443 ssl; # managed by Certbot 6 | 7 | # RSA certificate 8 | ssl_certificate /etc/letsencrypt/live/uid4oe.dev/fullchain.pem; # managed by Certbot 9 | ssl_certificate_key /etc/letsencrypt/live/uid4oe.dev/privkey.pem; # managed by Certbot 10 | 11 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 12 | 13 | # Redirect non-https traffic to https 14 | if ($scheme != "https") { 15 | return 301 https://$host$request_uri; 16 | } # managed by Certbot 17 | 18 | location / { 19 | root /usr/share/nginx/html; 20 | index index.html index.htm; 21 | try_files $uri /index.html$is_args$args =404; 22 | } 23 | 24 | location /api { 25 | proxy_pass http://bff-service:8080; 26 | } 27 | 28 | 29 | } 30 | 31 | 32 | server { 33 | listen 80; 34 | server_name go-grpc.uid4oe.dev www.go-grpc.uid4oe.dev; 35 | 36 | listen 443 ssl; # managed by Certbot 37 | 38 | # RSA certificate 39 | ssl_certificate /etc/letsencrypt/live/go-grpc.uid4oe.dev/fullchain.pem; # managed by Certbot 40 | ssl_certificate_key /etc/letsencrypt/live/go-grpc.uid4oe.dev/privkey.pem; # managed by Certbot 41 | 42 | include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot 43 | 44 | # Redirect non-https traffic to https 45 | if ($scheme != "https") { 46 | return 301 https://$host$request_uri; 47 | } # managed by Certbot 48 | 49 | location / { 50 | root /usr/share/nginx/html; 51 | index index.html index.htm; 52 | try_files $uri /index.html$is_args$args =404; 53 | } 54 | 55 | location /api { 56 | proxy_pass http://bff-service:8080; 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microservices-grpc-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "react": "^17.0.1", 10 | "react-dom": "^17.0.1", 11 | "react-router-dom": "^5.2.0", 12 | "react-scripts": "4.0.1", 13 | "semantic-ui-css": "^2.4.1", 14 | "semantic-ui-react": "^2.0.3", 15 | "web-vitals": "^0.2.4" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "rocket": "react-scripts build && firebase deploy", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uid4oe/microservices-grpc-ui/5a1243bcaea079de3131218bf89acdd10082fcaf/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | Microservices with gRPC 25 | 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, createContext } from "react"; 2 | import { Switch, Route, Redirect, useHistory, useLocation } from "react-router-dom"; 3 | import { Button, Card, Grid, Header, Menu, Popup } from "semantic-ui-react"; 4 | import MainPage from "./pages/main-page" 5 | import { ADVICES, PROTOCOL, USERS, HOSTS, PORTS } from "./configuration"; 6 | export const BackendContext = createContext(null); 7 | 8 | function App() { 9 | 10 | const history = useHistory(); 11 | const location = useLocation(); 12 | 13 | const backendList = ["Go", "Kotlin", "Java"]; 14 | 15 | const backendConfig = [ 16 | { 17 | users: PROTOCOL + HOSTS[0] + PORTS[0] + USERS, 18 | advices: PROTOCOL + HOSTS[0] + PORTS[0] + ADVICES, 19 | active: 0, 20 | }, 21 | { 22 | users: PROTOCOL + HOSTS[0] + PORTS[1] + USERS, 23 | advices: PROTOCOL + HOSTS[0] + PORTS[1] + ADVICES, 24 | active: 1, 25 | }, 26 | { 27 | users: PROTOCOL + HOSTS[0] + PORTS[2] + USERS, 28 | advices: PROTOCOL + HOSTS[0] + PORTS[2] + ADVICES, 29 | active: 2, 30 | }, 31 | ] 32 | 33 | const index = backendList.map(i => i.toUpperCase()).indexOf(location.pathname.substring(1).toUpperCase()); 34 | const [backend, setBackend] = useState(backendConfig[Math.max(index, 0)]); 35 | 36 | // HOSTS = local,go,kt,java 37 | const changeBackend = (e, { name }) => { 38 | setBackend( 39 | backendConfig[name] 40 | ); 41 | history.push(backendList[name].toLowerCase()) 42 | } 43 | 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 | Microservices gRPC 57 |
58 |
59 | 60 | 61 | 62 | {backendList.map((item, index) => 63 | 68 | {item} 69 | 70 | )} 71 | }>Click to change backend! 72 | 73 | 74 | 82 | 90 | 91 |
92 |
93 |
94 |
95 |
96 |
97 | 98 | 99 | 100 | 101 | 102 | MainPage()} /> 103 | MainPage()} /> 104 | MainPage()} /> 105 | 106 | 107 | 108 | 109 | 110 | 111 |
112 | 113 |
114 | ); 115 | } 116 | 117 | export default App; 118 | -------------------------------------------------------------------------------- /src/components/selected-user-card-component.js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from "react"; 2 | import { Button, Card, Dimmer, Form, Grid, Header, Icon, Image, Label, Loader, Message, Popup, Segment } from "semantic-ui-react"; 3 | import { UpdateContext } from "../pages/main-page"; 4 | import { EXTERNAL_ADVICE_API, HEADERS, images } from "../configuration"; 5 | import { BackendContext } from "../App"; 6 | 7 | const emptyAdvice = { advice: "", cached: false }; 8 | 9 | const SelectedUserCard = ({ user }) => { 10 | const [values, setValues] = useState(null); 11 | const [loading, setLoading] = useState(false); 12 | const [cardLoading, setCardLoading] = useState(false); 13 | const [error, setError] = useState(false); 14 | const [responseTime, setResponseTime] = useState(0); 15 | const [adviceResponseTime, setAdviceResponseTime] = useState(0); 16 | const [calledResource, setCalledResource] = useState(""); 17 | const [remoteAdvice, setRemoteAdvice] = useState(emptyAdvice); 18 | 19 | const backend = useContext(BackendContext); 20 | 21 | const newUser = user?.newUser; 22 | 23 | const getRemoteAdvice = async () => { 24 | const response = await fetch(EXTERNAL_ADVICE_API); 25 | if (response.ok) { 26 | const { slip: { advice } } = await response.json(); 27 | return advice 28 | } 29 | } 30 | 31 | const updateCurrentAdvice = (data) => { 32 | setRemoteAdvice({ advice: data, cached: true }) 33 | setTimeout(() => setRemoteAdvice({ ...remoteAdvice, cached: false }), 2000) 34 | } 35 | 36 | if (newUser && remoteAdvice?.advice?.length === 0) { 37 | getRemoteAdvice().then((data) => setRemoteAdvice({ advice: data, cached: false })); 38 | } 39 | 40 | const [updated, setUpdated] = useContext(UpdateContext) 41 | 42 | useEffect(() => { 43 | setResponseTime(0); 44 | setCardLoading(false); 45 | setError(false); 46 | 47 | const getDetails = async () => { 48 | setCardLoading(true); 49 | const startTime = new Date(); 50 | const response = await fetch(`${backend.users}${user.id}`); 51 | const endTime = new Date(); 52 | 53 | const { data, error } = await response.json() 54 | 55 | if (!response.ok) { 56 | setValues(null) 57 | setError(error) 58 | setTimeout(() => setError(false), 1500) 59 | } else { 60 | setValues({ ...user, ...data }) 61 | } 62 | setResponseTime(endTime - startTime) 63 | setCardLoading(false) 64 | 65 | } 66 | if (newUser) { 67 | setCalledResource("Create User"); 68 | setValues(user) 69 | } else { 70 | setCalledResource("Get User Details") 71 | getDetails() 72 | } 73 | }, [user, newUser]); 74 | 75 | const submitForm = async () => { 76 | setLoading(true) 77 | setError(false) 78 | setResponseTime(0) 79 | 80 | const url = newUser ? backend.users : backend.users + values.id; 81 | const method = newUser ? "POST" : "PUT"; 82 | !newUser && setCalledResource("Update User"); 83 | const startTime = new Date(); 84 | 85 | const response = await fetch(url, 86 | { 87 | body: JSON.stringify({ ...values, age: Number(values.age), salary: Number(values.salary), advice: remoteAdvice.advice }), 88 | method: method, 89 | ...HEADERS 90 | } 91 | ) 92 | const endTime = new Date(); 93 | if (!response.ok) { 94 | const { error } = await response.json() 95 | setError(error) 96 | setTimeout(() => setError(false), 1500) 97 | } else { 98 | setRemoteAdvice(emptyAdvice); 99 | setUpdated(!updated) 100 | } 101 | setLoading(false) 102 | setResponseTime(endTime - startTime) 103 | 104 | } 105 | 106 | const submitAdviceUpdate = async (data) => { 107 | setAdviceResponseTime(0) 108 | setError(false) 109 | 110 | const startTime = new Date(); 111 | const response = await fetch(backend.advices, 112 | { 113 | body: JSON.stringify({ user_id: values.id, advice: data }), 114 | method: "PUT", 115 | ...HEADERS 116 | } 117 | ) 118 | const endTime = new Date(); 119 | if (!response.ok) { 120 | const { error } = await response.json() 121 | setError(error) 122 | setTimeout(() => setError(false), 1500) 123 | } 124 | setAdviceResponseTime(endTime - startTime) 125 | } 126 | 127 | return 128 | {!cardLoading ? 129 | 130 | 131 |
132 | {calledResource} 133 | 136 |
137 |
138 | {error && 139 | 140 | 141 |
142 |
143 | } 144 | {values && 145 | 146 |
147 | {Object.keys(values).slice(1, 4).map(key => 148 | 149 | setValues({ ...values, [target.name]: target.value })} /> 150 | )} 151 | {Object.keys(values).slice(4, 6).map(key => 152 | 153 | setValues({ ...values, [target.name]: target.value })} /> 154 | )} 155 | 156 | 157 |
158 | } 159 | 160 | 161 | 162 | {!newUser && values && 163 | <> 164 | 165 | 166 | 167 | } 168 | 169 | {values && <> 170 | 171 | { 173 | getRemoteAdvice().then((data) => { 174 | updateCurrentAdvice(data); 175 | setValues({ ...values, advice: data }) 176 | submitAdviceUpdate(data) 177 | }) 178 | 179 | }} > 180 | 181 | 182 | }> 183 | Disabled until External API's request cache expires (2 seconds) 184 | 185 | 186 | }> 187 | {values?.advice ? values.advice : "Create a new user for my advice"} 188 | 189 |
Update Advice
190 | 193 | } 194 |
195 |
196 |
197 | 198 |
: 199 | 200 | 201 | 202 | 203 | 204 | 205 |
206 |
207 |
208 |
209 | } 210 |
211 | } 212 | 213 | export default SelectedUserCard; 214 | -------------------------------------------------------------------------------- /src/components/user-card-component.js: -------------------------------------------------------------------------------- 1 | import { Card, Grid, Image, Popup } from "semantic-ui-react"; 2 | 3 | const UserCard = ({ user, onClick }) => { 4 | return 8 | 9 | 10 | 11 | {user.name} 12 | 13 | 14 | 15 | 16 | 17 | 18 | Age 19 | 20 | 21 | {user.age} 22 | 23 | 24 | 25 | 26 | 27 | } /> 28 | } 29 | 30 | export default UserCard; 31 | -------------------------------------------------------------------------------- /src/configuration.js: -------------------------------------------------------------------------------- 1 | import gopher from "./gopher.png"; 2 | import duke from "./duke.png"; 3 | import unoffical_kt from "./unoffical_kt.png"; 4 | 5 | export const PROTOCOL = "http://"; 6 | export const HOSTS = ["localhost:", 7 | "ec2-18-192-156-217.eu-central-1.compute.amazonaws.com:", 8 | "ec2-3-64-31-58.eu-central-1.compute.amazonaws.com:", 9 | "ec2-3-68-243-90.eu-central-1.compute.amazonaws.com:"]; 10 | export const PORTS = ["8080", "8090", "8100"]; 11 | 12 | export const USERS = "/api/users/"; 13 | export const ADVICES = "/api/advices/"; 14 | 15 | export const EXTERNAL_ADVICE_API = "https://api.adviceslip.com/advice"; 16 | 17 | export const HEADERS = { 18 | headers: { 19 | 'Content-Type': 'application/json;charset=utf-8' 20 | } 21 | } 22 | 23 | export const images = [ 24 | gopher, 25 | unoffical_kt, 26 | duke 27 | ] 28 | 29 | 30 | export const DEFAULT_USER = { id: "", name: "", age: 0, greeting: "", salary: 0, power: "", newUser: true } 31 | export const SERVICES_DOWN = "Oops! Looks like services for the selected backend is not running."; -------------------------------------------------------------------------------- /src/duke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uid4oe/microservices-grpc-ui/5a1243bcaea079de3131218bf89acdd10082fcaf/src/duke.png -------------------------------------------------------------------------------- /src/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uid4oe/microservices-grpc-ui/5a1243bcaea079de3131218bf89acdd10082fcaf/src/gopher.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import reportWebVitals from "./reportWebVitals"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import "semantic-ui-css/semantic.min.css"; 7 | import App from "./App"; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById("root") 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /src/pages/main-page.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from "react"; 2 | 3 | import { Button, Grid, Card, Message, Segment, Dimmer, Loader, Header, Label } from "semantic-ui-react"; 4 | import { BackendContext } from "../App"; 5 | import SelectedUserCard from "../components/selected-user-card-component"; 6 | import UserCard from "../components/user-card-component"; 7 | import { DEFAULT_USER, SERVICES_DOWN } from "../configuration"; 8 | export const UpdateContext = createContext(null); 9 | 10 | const MainPage = () => { 11 | const [loaded, setLoaded] = useState(false); 12 | const [selectedUser, setSelectedUser] = useState({ ...DEFAULT_USER, newUser: true }); 13 | const [users, setUsers] = useState([]); 14 | const [updated, setUpdated] = useState(false); 15 | const [error, setError] = useState(false); 16 | const [responseTime, setResponseTime] = useState(0); 17 | 18 | const backend = useContext(BackendContext); 19 | 20 | const onGetRandomClick = () => { 21 | users?.length && setSelectedUser(users[Math.floor(Math.random() * users.length)]) 22 | }; 23 | 24 | const onGetAll = async () => { 25 | setLoaded(false); 26 | setError(false); 27 | setResponseTime(0); 28 | 29 | try { 30 | const startTime = new Date(); 31 | const response = await fetch(backend.users); 32 | const endTime = new Date(); 33 | const { data, error } = await response.json() 34 | 35 | if (!response.ok) { 36 | setUsers([]) 37 | setError(error) 38 | } else { 39 | setUsers(data?.reverse()) 40 | } 41 | setLoaded(true) 42 | setResponseTime(endTime - startTime) 43 | } 44 | catch (e) { 45 | setLoaded(true) 46 | setError(SERVICES_DOWN) 47 | } 48 | } 49 | 50 | 51 | useEffect(() => { 52 | onGetAll(); 53 | }, [backend, updated]) 54 | 55 | 56 | useEffect(() => { 57 | setSelectedUser({ ...DEFAULT_USER, newUser: true }) 58 | }, [backend]) 59 | 60 | return ( 61 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |
93 | Get Users 94 | 97 |
98 |
99 | {error ? 100 | 105 | : 106 | 107 | {loaded ? 108 | <> 109 | {users?.length ? 110 | 111 | {users.map(item => setSelectedUser(item)} />)} 112 | : <>No data} 113 | 114 | : 115 | 116 | 117 | 118 | 119 |
120 |
121 | } 122 |
} 123 |
124 |
125 |
126 |
127 | ); 128 | }; 129 | 130 | export default MainPage; 131 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/unoffical_kt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uid4oe/microservices-grpc-ui/5a1243bcaea079de3131218bf89acdd10082fcaf/src/unoffical_kt.png -------------------------------------------------------------------------------- /ui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uid4oe/microservices-grpc-ui/5a1243bcaea079de3131218bf89acdd10082fcaf/ui.gif -------------------------------------------------------------------------------- /ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uid4oe/microservices-grpc-ui/5a1243bcaea079de3131218bf89acdd10082fcaf/ui.png --------------------------------------------------------------------------------