├── .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 | 
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 |
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 |
190 |
191 | {adviceResponseTime} ms
192 |
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 |
95 | {responseTime} ms
96 |
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
--------------------------------------------------------------------------------