Compare commits

...

38 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
4dc99b740b Merge branch 'networking' 2024-01-16 22:47:04 +01:00
248364a686 Update diagrams 2024-01-16 22:46:43 +01:00
cd57e11404 Isolate networks 2024-01-16 21:37:32 +01:00
99ea80c93c Remove debug print 2024-01-16 17:11:03 +01:00
46543c83b1 Tweaks 2024-01-16 17:07:09 +01:00
636efa9744 Frontend finished 2024-01-16 16:45:08 +01:00
d77be41b99 Product images 2024-01-16 16:14:38 +01:00
423b853b82 Products 2024-01-16 08:22:28 +01:00
cd5a93351b Products basics 2024-01-15 23:42:11 +01:00
8bc736114c Initial 2024-01-15 23:33:23 +01:00
f220d08800 Container replicas 2024-01-15 11:34:48 +01:00
daafd76d7c Add new products container 2024-01-15 00:04:46 +01:00
9b755d5775 Rename containers 2024-01-14 23:40:03 +01:00
e8bf362c6b Interservice communication 2024-01-14 22:56:06 +01:00
a27461cca8 App config 2024-01-14 22:20:52 +01:00
1f3195d992 Filter products per machine 2024-01-14 22:12:57 +01:00
018fd310cb Add envoy proxy 2024-01-14 12:45:30 +01:00
304ce0678a Products app 2024-01-14 11:47:50 +01:00
ff375ae3e8 Machines done 2024-01-14 11:36:39 +01:00
75 changed files with 28703 additions and 750 deletions

View File

@ -1 +1,83 @@
# 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)

View File

@ -25,7 +25,7 @@ psql -v ON_ERROR_STOP=1 --username pero --password "pero.000" --dbname komponira
INSERT INTO public.products (id, name, description, image) VALUES (5, 'Kava', 'Kielbasa landjaeger sausage capicola sirloin filet mignon doner t-bone. Swine corned beef turkey hamburger flank pork chop capicola prosciutto venison shoulder strip steak jowl. Tongue pork salami biltong doner chislic andouille ball tip strip steak prosciutto.', 'kava.jpeg');
INSERT INTO public.products (id, name, description, image) VALUES (6, 'Kava s mlijekom', 'Shoulder bacon flank chuck jowl hamburger swine fatback shank shankle t-bone buffalo leberkas cow.', 'kava-s-mlijekom.jpeg');
INSERT INTO public.products (id, name, description, image) VALUES (7, 'Kava bez šećera', 'Pork pig prosciutto shoulder, landjaeger drumstick andouille filet mignon pork chop tri-tip bresaola tail.', 'kava-bez-secera.jpeg');
INSERT INTO public.products (id, name, description, image) VALUES (8, 'Cappucino', 'Fatback frankfurter jowl capicola. Buffalo short loin pancetta cow ball tip chicken. Pork loin biltong filet mignon rump t-bone kielbasa tail hamburger jowl pancetta andouille short loin.', 'cappucino.jpeg');
INSERT INTO public.products (id, name, description, image) VALUES (8, 'Cappuccino', 'Fatback frankfurter jowl capicola. Buffalo short loin pancetta cow ball tip chicken. Pork loin biltong filet mignon rump t-bone kielbasa tail hamburger jowl pancetta andouille short loin.', 'cappuccino.jpeg');
INSERT INTO public.products (id, name, description, image) VALUES (9, 'Mocca', 'Capicola salami shoulder tri-tip chicken meatball. Tail meatball filet mignon, landjaeger meatloaf sirloin strip steak chicken capicola picanha cow andouille rump shoulder. Chuck buffalo doner short ribs bacon ground round pancetta flank picanha pork loin.', 'mocca.jpeg');
INSERT INTO public.products (id, name, description, image) VALUES (10, 'Sendvič sa sirom', 'Ball tip beef ribs shank ground round t-bone, strip steak leberkas chuck beef pancetta burgdoggen biltong doner swine brisket. Pork chop tail cow filet mignon salami spare ribs pork belly boudin. ', 'sendvic-sa-sirom.jpeg');
INSERT INTO public.products (id, name, description, image) VALUES (11, 'Sendvič sa šunkom', 'Alcatra meatball filet mignon bresaola landjaeger, ham tenderloin chicken t-bone cow ham hock sausage fatback. Ground round cow ball tip ham venison beef ribs pork loin shank.', 'sendvic-sa-sunkom.jpeg');

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

@ -1,10 +1,12 @@
version: "3.8"
services:
db:
build:
context: ./database
dockerfile: Dockerfile
ports:
- 55432:5432
networks:
- backend-net
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
@ -17,14 +19,68 @@ services:
build:
context: ./machines
dockerfile: Dockerfile
networks:
- backend-net
environment:
- APPPORT=4000
- DBHOST=db
- DBPORT=5432
- DBNAME=komponiranje
- DBUSER=pero
- DBPASSWORD=pero.000
ports:
- 3000:3000
- PRODUCTSAPPURL=http://proxy:10000
depends_on:
db:
condition: service_healthy
products-app:
build:
context: ./products
dockerfile: Dockerfile
deploy:
mode: replicated
replicas: 2
networks:
- backend-net
environment:
- APPPORT=4001
- DBHOST=db
- DBPORT=5432
- DBNAME=komponiranje
- DBUSER=pero
- DBPASSWORD=pero.000
depends_on:
db:
condition: service_healthy
proxy:
image: envoyproxy/envoy:v1.28-latest
networks:
- frontend-net
- backend-net
ports:
- "10000:10000"
volumes:
- ./proxy/envoy.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
networks:
- frontend-net
ports:
- "8080:80"
depends_on:
- proxy
networks:
frontend-net:
name: frontend-net
internal: false
backend-net:
name: backend-net
internal: true

3
frontend/.dockerignore Normal file
View File

@ -0,0 +1,3 @@
**/node_modules
**/build

23
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

25
frontend/Dockerfile Normal file
View File

@ -0,0 +1,25 @@
# stage 1: build node frontend
FROM node:21 as 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-lock.json .
COPY ./public ./public
COPY ./src ./src
RUN \
npm install && \
npm run build
# stage 2: build final image
FROM nginx:1.25-alpine
COPY --from=node-builder /node-builder/build/. /usr/share/nginx/html

36
frontend/Makefile Normal file
View File

@ -0,0 +1,36 @@
CONTAINER_NAME=frontend-app
IMAGE_NAME=komponiranje-frontend-app
run:
@npm start
.PHONY: build
build:
@npm run build
upgrade-packages:
@go get -u ./...
docker-build: clean
@docker build \
--progress=plain \
--tag $(IMAGE_NAME) \
.
docker-run:
@docker run \
--name $(CONTAINER_NAME) \
--publish 8080:80 \
--detach \
$(IMAGE_NAME)
clean:
- @docker stop $(CONTAINER_NAME)
- @docker rm $(CONTAINER_NAME)
- @docker rmi $(IMAGE_NAME)

70
frontend/README.md Normal file
View File

@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

18724
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
frontend/package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.15.4",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Komponiranje frontend demo" />
<title>Komponiranje frontend demo</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,8 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<comment version="3.0">
<caption>FILE PHOTO: Coca-cola soda is shown on display during a preview of a new Walmart Super Center prior to its opening in Compton, California,</caption>
<note>FILE PHOTO: Coca-cola soda is shown on display during a preview of a new Walmart Super Center prior to its opening in Compton, California, U.S., January 10, 2017. REUTERS/Mike Blake/File Photo</note>
<place/>
<categories/>
</comment>

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

25
frontend/src/App.js Normal file
View File

@ -0,0 +1,25 @@
import { ThemeProvider, createTheme } from "@mui/material/styles";
import Container from "@mui/material/Container";
import Typography from "@mui/material/Typography";
import { Home } from "./pages/Home";
const darkTheme = createTheme({
palette: {
mode: "light",
},
});
function App() {
return (
<ThemeProvider theme={darkTheme}>
<Container fixed>
<Typography variant="h2" gutterBottom>
Komponiranje frontend demo
</Typography>
<Home />
</Container>
</ThemeProvider>
);
}
export default App;

View File

@ -0,0 +1,30 @@
import axios from "axios";
export class ApiBase {}
const commonHeaders = {
"Content-Type": "application/json",
Accept: "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Max-Age": 60,
"x-timezone": Intl.DateTimeFormat().resolvedOptions().timeZone,
};
export const axiosUnauthorizedInstance = axios.create({
timeout: 5000,
headers: commonHeaders,
});
axiosUnauthorizedInstance.interceptors.response.use(
(response) => response,
(error) => {
return Promise.reject(error);
}
);
export const axiosInstance = axios.create({
timeout: 5000,
headers: {
...commonHeaders,
},
});

View File

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

View File

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

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

@ -0,0 +1,45 @@
import * as React from "react";
import { useState } from "react";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemText from "@mui/material/ListItemText";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
function Machines({ machines, onSelect }) {
const [selectedId, setSelectedId] = useState(null);
const handleOnClick = (machineName, machineId) => {
onSelect(machineName, machineId);
setSelectedId(machineId);
};
const machineItems = machines.map((machine) => {
return (
<ListItem key={machine.id} disablePadding>
<ListItemButton
selected={selectedId === machine.id}
onClick={(event) => handleOnClick(machine.name, machine.id)}
>
<ListItemText primary={machine.name} />
</ListItemButton>
</ListItem>
);
});
return (
<>
<Typography variant="h4" gutterBottom>
Machines
</Typography>
<List>
<Paper>{machineItems}</Paper>
</List>
</>
);
}
export { Machines };

View File

@ -0,0 +1,30 @@
import * as React from "react";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import CardMedia from "@mui/material/CardMedia";
import Typography from "@mui/material/Typography";
import { CardActionArea } from "@mui/material";
import { PRODUCT_IMAGE_DIR } from "../const";
function ProductCard({ product, onClick }) {
const productImg = `${PRODUCT_IMAGE_DIR}/${product.image}`;
return (
<Card sx={{ width: "100%" }}>
<CardActionArea
onClick={() => {
onClick(product.id);
}}
>
<CardMedia component="img" height="200" image={productImg} alt={product.name} />
<CardContent>
<Typography gutterBottom variant="h5" component="div" sx={{ marginBottom: 0 }}>
{product.name}
</Typography>
</CardContent>
</CardActionArea>
</Card>
);
}
export { ProductCard };

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

@ -0,0 +1,50 @@
import * as React from "react";
import Grid from "@mui/material/Unstable_Grid2";
import Typography from "@mui/material/Typography";
import { ProductCard } from "./ProductCard";
import { ProductModal } from "./ProductModal";
import { productsApi } from "../api";
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) => {
return (
<Grid md={6} key={product.id} sx={{ display: "flex" }}>
<ProductCard product={product} onClick={onProductSelect} />
</Grid>
);
});
return (
<>
<Typography variant="h4" gutterBottom>
Products - {machineName}
</Typography>
<Grid container spacing={2}>
{productItems}
</Grid>
{productModal.isOpen && <ProductModal product={productModal.product} onClose={onProductModalClose} />}
</>
);
}
export { Products };

3
frontend/src/const.js Normal file
View File

@ -0,0 +1,3 @@
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/";

13
frontend/src/index.css Normal file
View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

17
frontend/src/index.js Normal file
View File

@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@ -0,0 +1,41 @@
import * as React from "react";
import { useState, useEffect } from "react";
import Grid from "@mui/material/Unstable_Grid2";
import { machinesApi } from "../api";
import { Machines } from "../components/Machines";
import { Products } from "../components/Products";
function Home() {
const [machines, setMachines] = useState([]);
const [productsData, setProductsData] = useState(null);
useEffect(() => {
machinesApi.list().then((response) => {
setMachines(response.data.machines);
});
}, []);
const onMachineSelect = (machineName, machineId) => {
machinesApi.listProducts(machineId).then((response) => {
setProductsData({
machineName,
products: response.data.products,
});
});
};
return (
<Grid container spacing={2}>
<Grid md={3}>
<Machines machines={machines} onSelect={onMachineSelect} />
</Grid>
<Grid md={9}>
{productsData && <Products machineName={productsData.machineName} products={productsData.products} />}
</Grid>
</Grid>
);
}
export { Home };

View File

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -1,4 +1,4 @@
# stage 2: build golang backend
# stage 1: build golang backend
FROM golang:1.21-alpine3.19 as go-builder
WORKDIR /go-builder
@ -13,8 +13,9 @@ RUN \
# stage 2: build final container
FROM alpine:3.19
USER $USER
WORKDIR /app
COPY --from=go-builder /go-builder/machines-app /app
USER 33
ENTRYPOINT ["/app/machines-app"]

View File

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

View File

@ -0,0 +1,70 @@
package api
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func isError(c *gin.Context, err error) bool {
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"details": err.Error(),
})
return true
}
return false
}
func raiseError(c *gin.Context, errCode int, message string) {
c.AbortWithStatusJSON(errCode, gin.H{
"details": message,
})
}
func raiseBadRequestError(c *gin.Context, message string) {
raiseError(c, http.StatusBadRequest, message)
}
func raiseNotFoundError(c *gin.Context, message string) {
raiseError(c, http.StatusNotFound, message)
}
func raiseInternalError(c *gin.Context, message string) {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"details": "Internal server error. We will we will fix it!",
})
}
func corsMiddleware() gin.HandlerFunc {
allowHeaders := [12]string{
"Content-Type",
"Content-Length",
"Accept-Encoding",
"X-CSRF-Token",
"Authorization",
"accept",
"origin",
"Cache-Control",
"X-Requested-With",
"x-timezone",
"Access-Control-Allow-Origin",
"Access-Control-Max-Age",
}
return func(c *gin.Context) {
allowOrigin := "*"
c.Writer.Header().Set("Access-Control-Allow-Origin", allowOrigin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", strings.Join(allowHeaders[:], ", "))
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}

View File

@ -2,16 +2,16 @@ package api
import (
"fmt"
"io"
"machines/app/cfg"
"machines/app/db"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
const HOST = "0.0.0.0"
const PORT = 3000
func handlePing(c *gin.Context) {
c.JSON(
http.StatusOK,
@ -24,30 +24,94 @@ func handlePing(c *gin.Context) {
func handleGetMachines(dbConn *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
machines := db.GetMachines(dbConn)
fmt.Printf("%+v\n", machines)
c.JSON(
http.StatusOK,
GetMachinesResponse{
Machines: machines,
Machines: *machines,
},
)
}
}
func handleGetMachine(dbConn *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
machineId, err := strconv.Atoi(c.Param("machineId"))
if err != nil {
raiseBadRequestError(c, "Invalid machineId parameter")
return
}
machine, err := db.GetMachine(dbConn, machineId)
if err != nil {
raiseNotFoundError(c, "Machine not found")
return
}
c.JSON(
http.StatusOK,
machine,
)
}
}
func handleGetMachineProducts(dbConn *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
machineId, err := strconv.Atoi(c.Param("machineId"))
if err != nil {
raiseBadRequestError(c, "Invalid machineId parameter")
return
}
machine, err := db.GetMachine(dbConn, machineId)
if err != nil {
raiseNotFoundError(c, "Machine not found")
return
}
url := fmt.Sprintf("%s/products?machineId=%d", cfg.Config.ProductsAppUrl, machine.Id)
resp, err := http.Get(url)
if err != nil {
fmt.Println(err.Error())
raiseInternalError(c, err.Error())
return
}
if resp.Body != nil {
defer resp.Body.Close()
}
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println(err.Error())
raiseInternalError(c, err.Error())
return
}
c.Header("Content-Type", "application/json; charset=utf-8")
c.Writer.WriteString(string(body))
}
}
func initRouter(dbConn *gorm.DB) *gin.Engine {
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
router.Use(corsMiddleware())
router.GET("/ping", handlePing)
router.GET("/", handleGetMachines(dbConn))
// routes.GET("/machines/:machineId", handleGetMachineDetails)
routes := router.Group("/machines")
{
routes.GET("/ping", handlePing)
routes.GET("", handleGetMachines(dbConn))
routes.GET("/:machineId", handleGetMachine(dbConn))
routes.GET("/:machineId/products", handleGetMachineProducts(dbConn))
}
return router
}
func Serve(dbConn *gorm.DB) {
serverAddr := fmt.Sprintf("%s:%d", HOST, PORT)
serverAddr := fmt.Sprintf("%s:%d", cfg.Config.AppHost, cfg.Config.AppPort)
fmt.Printf("Starting serving on %s\n", serverAddr)
router := initRouter(dbConn)
router.Run(serverAddr)

View File

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

View File

@ -2,10 +2,21 @@ package db
import "gorm.io/gorm"
func GetMachines(dbConn *gorm.DB) []Machine {
func GetMachines(dbConn *gorm.DB) *[]Machine {
var machines []Machine
dbConn.Order("name").Find(&machines)
return machines
return &machines
}
func GetMachine(dbConn *gorm.DB, id int) (*Machine, error) {
var machine Machine
result := dbConn.Order("name").Where("id = ?", id).First(&machine)
if result.Error != nil {
return nil, result.Error
}
return &machine, nil
}

View File

@ -4,6 +4,7 @@ go 1.21.5
require (
github.com/gin-gonic/gin v1.9.1
github.com/kelseyhightower/envconfig v1.4.0
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
)
@ -24,7 +25,6 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect

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

947
package-lock.json generated Normal file
View File

@ -0,0 +1,947 @@
{
"name": "komponiranje",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.15.4"
}
},
"node_modules/@babel/code-frame": {
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
"integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
"dependencies": {
"@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
"integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
"dependencies": {
"@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
"integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
"integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.23.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz",
"integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
"integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.1",
"@emotion/memoize": "^0.8.1",
"@emotion/serialize": "^1.1.2",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/cache": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
"integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==",
"dependencies": {
"@emotion/memoize": "^0.8.1",
"@emotion/sheet": "^1.2.2",
"@emotion/utils": "^1.2.1",
"@emotion/weak-memoize": "^0.3.1",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
"integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz",
"integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==",
"dependencies": {
"@emotion/memoize": "^0.8.1"
}
},
"node_modules/@emotion/memoize": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
},
"node_modules/@emotion/react": {
"version": "11.11.3",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz",
"integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/cache": "^11.11.0",
"@emotion/serialize": "^1.1.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
"@emotion/utils": "^1.2.1",
"@emotion/weak-memoize": "^0.3.1",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz",
"integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==",
"dependencies": {
"@emotion/hash": "^0.9.1",
"@emotion/memoize": "^0.8.1",
"@emotion/unitless": "^0.8.1",
"@emotion/utils": "^1.2.1",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz",
"integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA=="
},
"node_modules/@emotion/styled": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz",
"integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/is-prop-valid": "^1.2.1",
"@emotion/serialize": "^1.1.2",
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
"@emotion/utils": "^1.2.1"
},
"peerDependencies": {
"@emotion/react": "^11.0.0-rc.0",
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/unitless": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
"integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz",
"integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg=="
},
"node_modules/@emotion/weak-memoize": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
},
"node_modules/@floating-ui/core": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.3.tgz",
"integrity": "sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q==",
"dependencies": {
"@floating-ui/utils": "^0.2.0"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.4.tgz",
"integrity": "sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==",
"dependencies": {
"@floating-ui/core": "^1.5.3",
"@floating-ui/utils": "^0.2.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.5.tgz",
"integrity": "sha512-UsBK30Bg+s6+nsgblXtZmwHhgS2vmbuQK22qgt2pTQM6M3X6H1+cQcLXqgRY3ihVLcZJE6IvqDQozhsnIVqK/Q==",
"dependencies": {
"@floating-ui/dom": "^1.5.4"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
},
"node_modules/@mui/base": {
"version": "5.0.0-beta.31",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.31.tgz",
"integrity": "sha512-+uNbP3OHJuZVI00WyMg7xfLZotaEY7LgvYXDfONVJbrS+K9wyjCIPNfjy8r9XJn4fbHo/5ibiZqjWnU9LMNv+A==",
"dependencies": {
"@babel/runtime": "^7.23.7",
"@floating-ui/react-dom": "^2.0.5",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.4",
"@popperjs/core": "^2.11.8",
"clsx": "^2.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "5.15.4",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.4.tgz",
"integrity": "sha512-0OZN9O6hAtBpx70mMNFOPaAIol/ytwZYPY+z7Rf9dK3+1Xlzwvj5/IeShJKvtp76S1qJyhPuvZg0+BGqQaUnUw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/material": {
"version": "5.15.4",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.4.tgz",
"integrity": "sha512-T/LGRAC+M0c+D3+y67eHwIN5bSje0TxbcJCWR0esNvU11T0QwrX3jedXItPNBwMupF2F5VWCDHBVLlFnN3+ABA==",
"dependencies": {
"@babel/runtime": "^7.23.7",
"@mui/base": "5.0.0-beta.31",
"@mui/core-downloads-tracker": "^5.15.4",
"@mui/system": "^5.15.4",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.4",
"@types/react-transition-group": "^4.4.10",
"clsx": "^2.1.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1",
"react-is": "^18.2.0",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/private-theming": {
"version": "5.15.4",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.4.tgz",
"integrity": "sha512-9N5myIMEEQTM5WYWPGvvYADzjFo12LgJ7S+2iTZkBNOcJpUxQYM1tvYjkHCDV+t1ocMOEgjR2EfJ9Dus30dBlg==",
"dependencies": {
"@babel/runtime": "^7.23.7",
"@mui/utils": "^5.15.4",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/styled-engine": {
"version": "5.15.4",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.4.tgz",
"integrity": "sha512-vtrZUXG5XI8CNiNLcxjIirW4dEbOloR+ikfm6ePBo7jXpJdpXjVzBWetrfE+5eI0cHkKWlTptnJ2voKV8pBRfw==",
"dependencies": {
"@babel/runtime": "^7.23.7",
"@emotion/cache": "^11.11.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/system": {
"version": "5.15.4",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.4.tgz",
"integrity": "sha512-KCwkHajGBXPs2TK1HJjIyab4NDk0cZoBDYN/TTlXVo1qBAmCjY0vjqrlsjeoG+wrwwcezXMLs/e6OGP66fPCog==",
"dependencies": {
"@babel/runtime": "^7.23.7",
"@mui/private-theming": "^5.15.4",
"@mui/styled-engine": "^5.15.4",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.4",
"clsx": "^2.1.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/types": {
"version": "7.2.13",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz",
"integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==",
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils": {
"version": "5.15.4",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.4.tgz",
"integrity": "sha512-E2wLQGBcs3VR52CpMRjk46cGscC4cbf3Q2uyHNaAeL36yTTm+aVNbtsTCazXtjOP4BDd8lu6VtlTpVC8Rtl4mg==",
"dependencies": {
"@babel/runtime": "^7.23.7",
"@types/prop-types": "^15.7.11",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
},
"node_modules/@types/prop-types": {
"version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
},
"node_modules/@types/react": {
"version": "18.2.48",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz",
"integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
"integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/chalk/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/clsx": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"engines": {
"node": ">=4"
}
},
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/hoist-non-react-statics/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
},
"node_modules/is-core-module": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"dependencies": {
"hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dependencies": {
"callsites": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"engines": {
"node": ">=8"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
},
"peerDependencies": {
"react": "^18.2.0"
}
},
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"engines": {
"node": ">=4"
}
},
"node_modules/scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"engines": {
"node": ">=4"
}
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"engines": {
"node": ">= 6"
}
}
}
}

7
package.json Normal file
View File

@ -0,0 +1,7 @@
{
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.15.4"
}
}

37
products/.air.toml Normal file
View File

@ -0,0 +1,37 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./app/."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "build"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false

3
products/.dockerignore Normal file
View File

@ -0,0 +1,3 @@
**/build
**/tmp
**/.env*

9
products/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
/.vscode
/.idea
/build
/tmp
/config.yaml
/.env.production
/main

21
products/Dockerfile Normal file
View File

@ -0,0 +1,21 @@
# stage 1: build golang backend
FROM golang:1.21-alpine3.19 as go-builder
WORKDIR /go-builder
COPY . .
RUN \
go mod download && \
go mod verify && \
go build -v -ldflags "-s -w" -o products-app ./app/main.go
# stage 2: build final container
FROM alpine:3.19
WORKDIR /app
COPY --from=go-builder /go-builder/products-app /app
USER 33
ENTRYPOINT ["/app/products-app"]

38
products/Makefile Normal file
View File

@ -0,0 +1,38 @@
EXEC=products-app
CONTAINER_NAME=products-app
IMAGE_NAME=komponiranje-products-app
run:
@air
.PHONY: build
build:
@go build -ldflags "-s -w" -o ./build/${EXEC} ./app/.
upgrade-packages:
@go get -u ./...
docker-build: clean
@docker build \
--progress=plain \
--tag $(IMAGE_NAME) \
.
docker-run:
@docker run \
--name $(CONTAINER_NAME) \
--publish 4001:4001 \
--env DBPORT=55432 \
--detach \
$(IMAGE_NAME)
clean:
- @docker stop $(CONTAINER_NAME)
- @docker rm $(CONTAINER_NAME)
- @docker rmi $(IMAGE_NAME)

5
products/app/api/dto.go Normal file
View File

@ -0,0 +1,5 @@
package api
type PingDto struct {
Message string `json:"message"`
}

View File

@ -0,0 +1,70 @@
package api
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func isError(c *gin.Context, err error) bool {
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"details": err.Error(),
})
return true
}
return false
}
func raiseError(c *gin.Context, errCode int, message string) {
c.AbortWithStatusJSON(errCode, gin.H{
"details": message,
})
}
func raiseBadRequestError(c *gin.Context, message string) {
raiseError(c, http.StatusBadRequest, message)
}
func raiseNotFoundError(c *gin.Context, message string) {
raiseError(c, http.StatusNotFound, message)
}
func raiseInternalError(c *gin.Context, message string) {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"details": "Internal server error. We will we will fix it!",
})
}
func corsMiddleware() gin.HandlerFunc {
allowHeaders := [12]string{
"Content-Type",
"Content-Length",
"Accept-Encoding",
"X-CSRF-Token",
"Authorization",
"accept",
"origin",
"Cache-Control",
"X-Requested-With",
"x-timezone",
"Access-Control-Allow-Origin",
"Access-Control-Max-Age",
}
return func(c *gin.Context) {
allowOrigin := "*"
c.Writer.Header().Set("Access-Control-Allow-Origin", allowOrigin)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", strings.Join(allowHeaders[:], ", "))
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}

View File

@ -0,0 +1,7 @@
package api
import "products/app/db"
type GetProductsResponse struct {
Products []db.Product `json:"products"`
}

View File

@ -0,0 +1,97 @@
package api
import (
"fmt"
"net/http"
"products/app/cfg"
"products/app/db"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func handlePing(c *gin.Context) {
c.JSON(
http.StatusOK,
PingDto{
Message: "Pong!",
},
)
}
func handleGetProducts(dbConn *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var (
machineId *int = nil
err error
mId int
)
machineIdStr, ok := c.GetQuery("machineId")
if ok && len(machineIdStr) > 0 {
if mId, err = strconv.Atoi(machineIdStr); err != nil {
raiseBadRequestError(c, "Invalid machineId filter")
return
}
machineId = &mId
}
products := db.GetProducts(dbConn, machineId)
// for i := 0; i < len(*products); i++ {
// (*products)[i].Name = fmt.Sprintf("[local] %s", (*products)[i].Name)
// }
c.JSON(
http.StatusOK,
GetProductsResponse{
Products: *products,
},
)
}
}
func handleGetProduct(dbConn *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
productId, err := strconv.Atoi(c.Param("productId"))
if err != nil {
raiseBadRequestError(c, "Invalid productId parameter")
return
}
product, err := db.GetProduct(dbConn, productId)
if err != nil {
raiseNotFoundError(c, "Product not found")
return
}
c.JSON(
http.StatusOK,
product,
)
}
}
func initRouter(dbConn *gorm.DB) *gin.Engine {
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
router.Use(corsMiddleware())
routes := router.Group("/products")
{
routes.GET("/ping", handlePing)
routes.GET("", handleGetProducts(dbConn))
routes.GET("/:productId", handleGetProduct(dbConn))
}
return router
}
func Serve(dbConn *gorm.DB) {
serverAddr := fmt.Sprintf("%s:%d", cfg.Config.AppHost, cfg.Config.AppPort)
fmt.Printf("Starting serving on %s\n", serverAddr)
router := initRouter(dbConn)
router.Run(serverAddr)
}

View File

@ -0,0 +1,26 @@
package cfg
import (
"github.com/kelseyhightower/envconfig"
)
type configStruct struct {
AppHost string `default:"0.0.0.0"`
AppPort int `default:"4001"`
DbHost string `default:"localhost"`
DbPort int `default:"55432"`
DbName string `default:"komponiranje"`
DbUser string `default:"pero"`
DbPassword string `default:"pero.000"`
}
const ENV_PREFIX = ""
var Config configStruct
func init() {
err := envconfig.Process(ENV_PREFIX, &Config)
if err != nil {
panic(err)
}
}

30
products/app/db/db.go Normal file
View File

@ -0,0 +1,30 @@
package db
import (
"fmt"
"products/app/cfg"
"gorm.io/driver/postgres"
"gorm.io/gorm"
gormLogger "gorm.io/gorm/logger"
)
func ConnectDb() *gorm.DB {
var connectionString = fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable",
cfg.Config.DbUser,
cfg.Config.DbPassword,
cfg.Config.DbHost,
cfg.Config.DbPort,
cfg.Config.DbName,
)
var err error
dbConn, err := gorm.Open(postgres.Open(connectionString), &gorm.Config{
Logger: gormLogger.Default.LogMode(gormLogger.Info),
})
if err != nil {
panic("Error connecting to database: " + err.Error())
}
return dbConn
}

12
products/app/db/models.go Normal file
View File

@ -0,0 +1,12 @@
package db
type Product struct {
Id int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Image string `json:"image"`
}
func (m *Product) TableName() string {
return "products"
}

View File

@ -0,0 +1,31 @@
package db
import "gorm.io/gorm"
func GetProducts(dbConn *gorm.DB, machineId *int) *[]Product {
var (
products []Product
query = dbConn
)
if machineId != nil {
query = query.
Joins("LEFT JOIN machine_products mp ON mp.product_id = products.id").
Where("mp.machine_id = ?", machineId)
}
query.Order("name").Find(&products)
return &products
}
func GetProduct(dbConn *gorm.DB, id int) (*Product, error) {
var product Product
result := dbConn.Order("name").Where("id = ?", id).First(&product)
if result.Error != nil {
return nil, result.Error
}
return &product, nil
}

11
products/app/main.go Normal file
View File

@ -0,0 +1,11 @@
package main
import (
"products/app/api"
"products/app/db"
)
func main() {
dbConn := db.ConnectDb()
api.Serve(dbConn)
}

46
products/go.mod Normal file
View File

@ -0,0 +1,46 @@
module products
go 1.21.5
require (
github.com/gin-gonic/gin v1.9.1
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx/v5 v5.5.1 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

114
products/go.sum Normal file
View File

@ -0,0 +1,114 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

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

59
proxy/envoy.yaml Normal file
View File

@ -0,0 +1,59 @@
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
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: machines-app
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: products-app
port_value: 4001