Compare commits

..

19 Commits

Author SHA1 Message Date
c8fecc52d6 Update docs 2024-01-26 11:48:35 +01:00
e97cbc6ad2 Update docs 2024-01-26 11:27:24 +01:00
9fbca2677b Update readme 2024-01-26 11:05:20 +01:00
6daa4c284d Update readme 2024-01-26 11:01:19 +01:00
1c1eaa7ee7 Update docs 2024-01-26 10:50:45 +01:00
96501f712c Update diagram 2024-01-25 17:51:32 +01:00
ce52b5aff5 Update diagram 2024-01-25 17:48:44 +01:00
b9ad5fd922 Update diagrams 2024-01-23 23:04:23 +01:00
f6b1a7eede Merge branch 'product-details' 2024-01-23 23:01:14 +01:00
c1b2a8cbdd Merge branch 'main' into product-details 2024-01-23 23:00:53 +01:00
aa22b48746 Frontend env arguments 2024-01-23 23:00:03 +01:00
ea7c54a5dc Product detail modal 2024-01-23 23:00:03 +01:00
0a344ed1ce DC in local network 2024-01-23 22:59:30 +01:00
3d6e4e6f34 Move apps to different ports 2024-01-23 22:59:02 +01:00
2cdbebdfa1 Frontend env arguments 2024-01-23 22:55:21 +01:00
5db0026aa7 Product detail modal 2024-01-23 22:32:11 +01:00
7b213dd33a DC in local network 2024-01-23 21:40:57 +01:00
b69f26027d Move apps to different ports 2024-01-23 20:14:01 +01:00
95b0c5f1ad FE tweaks 2024-01-18 13:17:45 +01:00
28 changed files with 6111 additions and 1193 deletions

View File

@ -1 +1,83 @@
# Komponiranje # Komponiranje
## How to run
Clone repository
```
git clone https://gitea.ekirin.com/Intis/prezentacija-komponiranje.git
cd prezentacija-komponiranje
```
Start docker compose with all containers:
```
docker compose up -d
```
Wait until docker images are built and containers starts.
Browse to [Local frontend application on port 8080](http://localhost:8080)
List running containers:
```
docker compose ps
```
Attach to docker compose output:
```
docker compose logs -f
```
Stop running containers:
```
docker compose down
```
### Run on host network
Start docker compose:
```
docker compose -f docker-compose-local.yml up -d
```
Containers will listen to the following local ports:
| Service | Port |
| ------------- | ----- |
| FE / nginx | 80 |
| Envoy proxy | 10000 |
| Machines app | 4000 |
| Products app | 4001 |
| Database | 55432 |
Browse to [Local frontend application on port 80](http://localhost:80)
Stop running containers:
```
docker compose -f docker-compose-local.yml down
```
## Media
### Life without Docker Compose
![Life without Docker Compose](media/life-without-docker-compose.png)
### Containers Architecture
![Containers Architecture](media/containers-architecture.png)
### Networking - docker-compose.yml
![Networking - docker-compose.yml](media/networking.png)
### Networking - host network - docker-compose-local.yml
![Networking - host network - docker-compose-local.yml](media/networking-host-network.png)
### DCCT - Docker Compose Cloud Tester
- [DCCT repository](https://gitlab.televendcloud.com/cloud/dc-cloud-tester)
![DCCT - Docker Compose Cloud Tester](media/dcct.png)

66
docker-compose-local.yml Normal file
View File

@ -0,0 +1,66 @@
version: "3.8"
services:
db:
build:
context: ./database
dockerfile: Dockerfile
network_mode: "host"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
command: -p 55432
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -p 55432"]
interval: 1s
timeout: 5s
retries: 10
machines-app:
build:
context: ./machines
dockerfile: Dockerfile
network_mode: "host"
environment:
- APPPORT=4000
- DBHOST=localhost
- DBPORT=55432
- DBNAME=komponiranje
- DBUSER=pero
- DBPASSWORD=pero.000
- PRODUCTSAPPURL=http://localhost:10000
depends_on:
db:
condition: service_healthy
products-app:
build:
context: ./products
dockerfile: Dockerfile
network_mode: "host"
environment:
- APPPORT=4001
- DBHOST=localhost
- DBPORT=55432
- DBNAME=komponiranje
- DBUSER=pero
- DBPASSWORD=pero.000
depends_on:
db:
condition: service_healthy
proxy:
image: envoyproxy/envoy:v1.28-latest
network_mode: "host"
volumes:
- ./proxy/envoy-local.yaml:/etc/envoy/envoy.yaml
depends_on:
- machines-app
- products-app
frontend-app:
build:
context: ./frontend
dockerfile: Dockerfile
args:
- REACT_APP_MACHINES_API_URL=http://localhost:10000
- REACT_APP_PRODUCTS_API_URL=http://localhost:10000
network_mode: "host"
depends_on:
- proxy

View File

@ -7,8 +7,6 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
networks: networks:
- backend-net - backend-net
ports:
- 55432:5432
environment: environment:
- POSTGRES_USER=postgres - POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres - POSTGRES_PASSWORD=postgres
@ -24,7 +22,7 @@ services:
networks: networks:
- backend-net - backend-net
environment: environment:
- APPPORT=3000 - APPPORT=4000
- DBHOST=db - DBHOST=db
- DBPORT=5432 - DBPORT=5432
- DBNAME=komponiranje - DBNAME=komponiranje
@ -44,7 +42,7 @@ services:
networks: networks:
- backend-net - backend-net
environment: environment:
- APPPORT=3000 - APPPORT=4001
- DBHOST=db - DBHOST=db
- DBPORT=5432 - DBPORT=5432
- DBNAME=komponiranje - DBNAME=komponiranje
@ -69,10 +67,11 @@ services:
build: build:
context: ./frontend context: ./frontend
dockerfile: Dockerfile dockerfile: Dockerfile
args:
- REACT_APP_MACHINES_API_URL=http://localhost:10000
- REACT_APP_PRODUCTS_API_URL=http://localhost:10000
networks: networks:
- frontend-net - frontend-net
environment:
- REACT_APP_BACKEND_API_URL="http://localhost:10000"
ports: ports:
- "8080:80" - "8080:80"
depends_on: depends_on:

View File

@ -3,6 +3,12 @@ FROM node:21 as node-builder
WORKDIR /node-builder WORKDIR /node-builder
ARG REACT_APP_MACHINES_API_URL
ARG REACT_APP_PRODUCTS_API_URL
ENV REACT_APP_MACHINES_API_URL $REACT_APP_MACHINES_API_URL
ENV REACT_APP_PRODUCTS_API_URL $REACT_APP_PRODUCTS_API_URL
COPY ./package.json . COPY ./package.json .
COPY ./package-lock.json . COPY ./package-lock.json .
COPY ./public ./public COPY ./public ./public

View File

@ -1,43 +1,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta name="description" content="Komponiranje frontend demo" />
name="description" <title>Komponiranje frontend demo</title>
content="Web site created using create-react-app" </head>
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will <body>
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!-- </body>
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file. </html>
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -1,25 +1,8 @@
{ {
"short_name": "React App", "short_name": "React App",
"name": "Create React App Sample", "name": "Create React App Sample",
"icons": [ "start_url": "./index.html",
{ "display": "standalone",
"src": "favicon.ico", "theme_color": "#000000",
"sizes": "64x64 32x32 24x24 16x16", "background_color": "#ffffff"
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
} }

View File

@ -1,5 +1,4 @@
import axios from "axios"; import axios from "axios";
import { API_URL } from "../const";
export class ApiBase {} export class ApiBase {}
@ -12,7 +11,6 @@ const commonHeaders = {
}; };
export const axiosUnauthorizedInstance = axios.create({ export const axiosUnauthorizedInstance = axios.create({
baseURL: API_URL,
timeout: 5000, timeout: 5000,
headers: commonHeaders, headers: commonHeaders,
}); });
@ -25,7 +23,6 @@ axiosUnauthorizedInstance.interceptors.response.use(
); );
export const axiosInstance = axios.create({ export const axiosInstance = axios.create({
baseURL: API_URL,
timeout: 5000, timeout: 5000,
headers: { headers: {
...commonHeaders, ...commonHeaders,

View File

@ -1 +1,2 @@
export { machinesApi } from "./machines"; export { machinesApi } from "./machines";
export { productsApi } from "./products";

View File

@ -1,14 +1,17 @@
import { axiosInstance, ApiBase } from "./common"; import { axiosInstance, ApiBase } from "./common";
import { MACHINES_API_URL } from "../const";
const baseUrl = `${MACHINES_API_URL}/machines`;
class MachinesApi extends ApiBase { class MachinesApi extends ApiBase {
list = async () => { list = async () => {
return axiosInstance.get(`/machines`, {}); return axiosInstance.get(`${baseUrl}`, {});
}; };
get = async (machineId) => { get = async (machineId) => {
return axiosInstance.get(`/machines/${machineId}`, {}); return axiosInstance.get(`${baseUrl}/${machineId}`, {});
}; };
listProducts = async (machineId) => { listProducts = async (machineId) => {
return axiosInstance.get(`/machines/${machineId}/products`, {}); return axiosInstance.get(`${baseUrl}/${machineId}/products`, {});
}; };
} }

View File

@ -0,0 +1,12 @@
import { axiosInstance, ApiBase } from "./common";
import { PRODUCTS_API_URL } from "../const";
const baseUrl = `${PRODUCTS_API_URL}/products`;
class ProductsApi extends ApiBase {
get = async (productId) => {
return axiosInstance.get(`${baseUrl}/${productId}`, {});
};
}
export const productsApi = new ProductsApi();

View File

@ -6,20 +6,21 @@ import Typography from "@mui/material/Typography";
import { CardActionArea } from "@mui/material"; import { CardActionArea } from "@mui/material";
import { PRODUCT_IMAGE_DIR } from "../const"; import { PRODUCT_IMAGE_DIR } from "../const";
function ProductCard({ product }) { function ProductCard({ product, onClick }) {
const productImg = `${PRODUCT_IMAGE_DIR}/${product.image}`; const productImg = `${PRODUCT_IMAGE_DIR}/${product.image}`;
return ( return (
<Card> <Card sx={{ width: "100%" }}>
<CardActionArea> <CardActionArea
<CardMedia component="img" height="140" image={productImg} alt={product.name} /> onClick={() => {
onClick(product.id);
}}
>
<CardMedia component="img" height="200" image={productImg} alt={product.name} />
<CardContent> <CardContent>
<Typography gutterBottom variant="h5" component="div"> <Typography gutterBottom variant="h5" component="div" sx={{ marginBottom: 0 }}>
{product.name} {product.name}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary">
{product.description}
</Typography>
</CardContent> </CardContent>
</CardActionArea> </CardActionArea>
</Card> </Card>

View File

@ -0,0 +1,36 @@
import * as React from "react";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import CardMedia from "@mui/material/CardMedia";
import { PRODUCT_IMAGE_DIR } from "../const";
function ProductModal({ product, onClose }) {
const productImg = `${PRODUCT_IMAGE_DIR}/${product.image}`;
return (
<Dialog open={true} onClose={onClose}>
<DialogTitle>{product.name}</DialogTitle>
<DialogContent>
<CardMedia
component="img"
height="300"
image={productImg}
alt={product.name}
sx={{ marginBottom: "1rem" }}
/>
<DialogContentText>{product.description}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" autoFocus>
Close
</Button>
</DialogActions>
</Dialog>
);
}
export { ProductModal };

View File

@ -2,12 +2,32 @@ import * as React from "react";
import Grid from "@mui/material/Unstable_Grid2"; import Grid from "@mui/material/Unstable_Grid2";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { ProductCard } from "./ProductCard"; import { ProductCard } from "./ProductCard";
import { ProductModal } from "./ProductModal";
import { productsApi } from "../api";
function Products({ machineName, products, onSelect }) { function Products({ machineName, products, onSelect }) {
const [productModal, setProductModal] = React.useState({ isOpen: false, product: null });
const onProductSelect = (productId) => {
productsApi.get(productId).then((response) => {
setProductModal({
isOpen: true,
product: response.data,
});
});
};
const onProductModalClose = () => {
setProductModal({
isOpen: false,
productId: null,
});
};
const productItems = products.map((product) => { const productItems = products.map((product) => {
return ( return (
<Grid md={6} key={product.id} sx={{ display: "flex" }}> <Grid md={6} key={product.id} sx={{ display: "flex" }}>
<ProductCard product={product} /> <ProductCard product={product} onClick={onProductSelect} />
</Grid> </Grid>
); );
}); });
@ -21,6 +41,8 @@ function Products({ machineName, products, onSelect }) {
<Grid container spacing={2}> <Grid container spacing={2}>
{productItems} {productItems}
</Grid> </Grid>
{productModal.isOpen && <ProductModal product={productModal.product} onClose={onProductModalClose} />}
</> </>
); );
} }

View File

@ -1,2 +1,3 @@
export const API_URL = process.env.REACT_APP_BACKEND_API_URL || "http://localhost:10000"; export const MACHINES_API_URL = process.env.REACT_APP_MACHINES_API_URL || "http://localhost:4000";
export const PRODUCTS_API_URL = process.env.REACT_APP_PRODUCTS_API_URL || "http://localhost:4001";
export const PRODUCT_IMAGE_DIR = "/static/products/"; export const PRODUCT_IMAGE_DIR = "/static/products/";

View File

@ -18,9 +18,7 @@ function Home() {
}, []); }, []);
const onMachineSelect = (machineName, machineId) => { const onMachineSelect = (machineName, machineId) => {
console.log("selected:", machineName);
machinesApi.listProducts(machineId).then((response) => { machinesApi.listProducts(machineId).then((response) => {
console.log(response.data.products);
setProductsData({ setProductsData({
machineName, machineName,
products: response.data.products, products: response.data.products,

View File

@ -26,8 +26,7 @@ docker-build: clean
docker-run: docker-run:
@docker run \ @docker run \
--name $(CONTAINER_NAME) \ --name $(CONTAINER_NAME) \
--publish 3000:3000 \ --publish 4000:4000 \
--env CONTAINER_NAME="Awesome API server" \
--env DBPORT=55432 \ --env DBPORT=55432 \
--detach \ --detach \
$(IMAGE_NAME) $(IMAGE_NAME)

View File

@ -6,13 +6,13 @@ import (
type configStruct struct { type configStruct struct {
AppHost string `default:"0.0.0.0"` AppHost string `default:"0.0.0.0"`
AppPort int `default:"3000"` AppPort int `default:"4000"`
DbHost string `default:"localhost"` DbHost string `default:"localhost"`
DbPort int `default:"55432"` DbPort int `default:"55432"`
DbName string `default:"komponiranje"` DbName string `default:"komponiranje"`
DbUser string `default:"pero"` DbUser string `default:"pero"`
DbPassword string `default:"pero.000"` DbPassword string `default:"pero.000"`
ProductsAppUrl string `default:"http://localhost:3001"` ProductsAppUrl string `default:"http://localhost:4001"`
} }
const ENV_PREFIX = "" const ENV_PREFIX = ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
media/dcct.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

BIN
media/networking.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 KiB

View File

@ -26,8 +26,7 @@ docker-build: clean
docker-run: docker-run:
@docker run \ @docker run \
--name $(CONTAINER_NAME) \ --name $(CONTAINER_NAME) \
--publish 3000:3000 \ --publish 4001:4001 \
--env CONTAINER_NAME="Awesome API server" \
--env DBPORT=55432 \ --env DBPORT=55432 \
--detach \ --detach \
$(IMAGE_NAME) $(IMAGE_NAME)

View File

@ -39,6 +39,10 @@ func handleGetProducts(dbConn *gorm.DB) gin.HandlerFunc {
products := db.GetProducts(dbConn, machineId) products := db.GetProducts(dbConn, machineId)
// for i := 0; i < len(*products); i++ {
// (*products)[i].Name = fmt.Sprintf("[local] %s", (*products)[i].Name)
// }
c.JSON( c.JSON(
http.StatusOK, http.StatusOK,
GetProductsResponse{ GetProductsResponse{

View File

@ -6,7 +6,7 @@ import (
type configStruct struct { type configStruct struct {
AppHost string `default:"0.0.0.0"` AppHost string `default:"0.0.0.0"`
AppPort int `default:"3001"` AppPort int `default:"4001"`
DbHost string `default:"localhost"` DbHost string `default:"localhost"`
DbPort int `default:"55432"` DbPort int `default:"55432"`
DbName string `default:"komponiranje"` DbName string `default:"komponiranje"`

59
proxy/envoy-local.yaml Normal file
View File

@ -0,0 +1,59 @@
static_resources:
listeners:
- address:
socket_address:
address: 127.0.0.1
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: backend
domains:
- "*"
routes:
- match:
prefix: "/machines"
route:
cluster: machines-app
- match:
prefix: "/products"
route:
cluster: products-app
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: machines-app
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: machines-app
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 4000
- name: products-app
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: products-app
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 4001

View File

@ -43,7 +43,7 @@ static_resources:
address: address:
socket_address: socket_address:
address: machines-app address: machines-app
port_value: 3000 port_value: 4000
- name: products-app - name: products-app
connect_timeout: 0.25s connect_timeout: 0.25s
type: strict_dns type: strict_dns
@ -56,10 +56,4 @@ static_resources:
address: address:
socket_address: socket_address:
address: products-app address: products-app
port_value: 3000 port_value: 4001
admin:
access_log_path: "/dev/null"
address:
socket_address:
address: 0.0.0.0
port_value: 800