second exercice
This commit is contained in:
parent
f4b832c197
commit
2c89566c69
121
z2/README.md.txt
Normal file
121
z2/README.md.txt
Normal file
@ -0,0 +1,121 @@
|
||||
# Fruit app
|
||||
|
||||
## Overview of the APP
|
||||
|
||||
The application consist of two parts, an API and a front end. The application displays 5 fruits which are seeded to the database at the startup. The get API provides the list of fruits. Anyone can like a fruit. In this case the count of like will be increased.
|
||||
|
||||
![homepage](./src/images/homepage.png?raw=true 'System Architecture')
|
||||
|
||||
## Requirements
|
||||
|
||||
The basic system requirements are as follows
|
||||
|
||||
- Any OS, preferably Linux
|
||||
- Docker
|
||||
- minikube
|
||||
|
||||
### Technology used
|
||||
|
||||
- Flask (Backend Rest API)
|
||||
- Angular (Frontend)
|
||||
- MySQL - as persistent database
|
||||
- kubernetes
|
||||
- Docker
|
||||
|
||||
## Kubernetes environment
|
||||
|
||||
For shipping and deploying the application kubernetes(minikube) is used.
|
||||
|
||||
Key points of the kubernetes objects is given below.
|
||||
|
||||
### Namespace
|
||||
|
||||
- **Namespace**: We used namespace to group our project resources together and isolate theme.
|
||||
|
||||
### Deployment
|
||||
|
||||
- **backend:** Deployment used to run the backend image
|
||||
- **front** Deployment used to run the frontend image
|
||||
|
||||
### Statefulset:
|
||||
|
||||
Mysql database is deployed as a statefulset kuebernetes object. StatefulSet is used to manage stateful applications with persistent storage. Storage stays associated with replacement pods. Volumes persist when pods are deleted.. You can find more information about statefulset [here](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)
|
||||
|
||||
### Services
|
||||
|
||||
The kubernetes services are used to expose our application as a network service.
|
||||
|
||||
- **backend-service:** service used to communicate with the backend APO.
|
||||
- **mysql-service** runs the mysql database service required for the backend API
|
||||
- **front-service** this is the entrypoint for the front-end application
|
||||
|
||||
### ConfigMap:
|
||||
|
||||
A ConfigMap is used to store nginx configuration file. The front pod will consume the ConfigMaps as the default configuration files for nginx web server. This ConfigMap contains the rules for mapping the `/api` request to the backend service.
|
||||
|
||||
### PersistentVolumeClaim:
|
||||
|
||||
We used a persitent volume claim to bind the data volume (persistent volume) and the Pod (mysql pod).
|
||||
|
||||
### PersistentVolume:
|
||||
|
||||
We used a persitent volume in order to save the database data in a persistent volume which is independent of the lifecycle of the Pods. It means that data represented by a PV continue to exist as the cluster changes and as Pods are deleted and recreated.
|
||||
|
||||
## A description of the container configuration performed.
|
||||
|
||||
The most important part is the connection to between database and the backend and between the frontend and the backend. The credentials for mysql in the statefulset.yml file should match the ones in the deployment.yml file for the backend deployment.
|
||||
|
||||
For the connection between the frontend and backend, we used the nginx base image for our frontend image. Also ConfigMap is used to store nginx configuration file to forward all the `/api` request to the backend service.
|
||||
|
||||
## How to prepare, run, pause and delete the application.
|
||||
|
||||
### Running minikube:
|
||||
|
||||
First we should run the minikube and expose the dashboard
|
||||
|
||||
```
|
||||
minikube start
|
||||
minikube dashboard
|
||||
```
|
||||
|
||||
### Prepare the app
|
||||
|
||||
As the local docker registry is not reachable for minikube, we shoud proxy the docker daemon to the minikube registry before building our images, for this we have to run the following command.
|
||||
|
||||
```
|
||||
eval $(minikube docker-env)
|
||||
```
|
||||
|
||||
Now to prepare the application you have to run the command :
|
||||
|
||||
```
|
||||
sh prepare-app.sh
|
||||
```
|
||||
|
||||
The script will build the frontend and the backend images.
|
||||
|
||||
### Run the app
|
||||
|
||||
To run the app you have to run:
|
||||
|
||||
```
|
||||
sh start-app.sh
|
||||
```
|
||||
|
||||
the script which will create a namespace and all the required kubernetes object of the application.
|
||||
|
||||
### To stop the app
|
||||
|
||||
The `stop-app.sh` script will remove the namespace and its resources also it will remove the persistent volume we created in the run script.
|
||||
|
||||
## How to view the application on the web.
|
||||
|
||||
To expose the application service outside the kubernetes we used the `kubectl port-forwading` to forward the front end service localy .
|
||||
|
||||
```
|
||||
POD_NAME=$(kubectl get pods --namespace suhailahamed -l "app=fruitapp-frontend" -o jsonpath="{.items[0].metadata.name}")
|
||||
kubectl port-forward $POD_NAME 8000:80 --namespace suhailahamed
|
||||
|
||||
```
|
||||
|
||||
Now the application is accessible through: `http://localhost:8000`
|
113
z2/deployment.yml
Normal file
113
z2/deployment.yml
Normal file
@ -0,0 +1,113 @@
|
||||
#
|
||||
# Backend deployment
|
||||
#
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: fruitapp-backend-deployment
|
||||
namespace: suhailahamed
|
||||
labels:
|
||||
app: fruitapp-backend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: fruitapp-backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: fruitapp-backend
|
||||
spec:
|
||||
initContainers:
|
||||
- name: init-cont
|
||||
image: busybox:1.31
|
||||
command: ['sh', '-c',
|
||||
'echo -e "Checking for the availability of MySQL Server deployment"; while ! nc -z mysql-service 3306; do sleep 1; printf "-"; done; echo -e " >> MySQL DB Server has started";']
|
||||
containers:
|
||||
- name: fruitapp-backend
|
||||
image: fruitapp-backend:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
command: ['bash']
|
||||
args: ['scripts/entrypoint.sh']
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
env:
|
||||
- name: SQLALCHEMY_DATABASE_URI
|
||||
value: mysql://user:password@mysql-service/main
|
||||
- name: FLASK_APP
|
||||
value: app.py
|
||||
- name: MYSQL_DATABASE
|
||||
value: main
|
||||
- name: MYSQL_USER
|
||||
value: user
|
||||
- name: MYSQL_PASSWORD
|
||||
value: password
|
||||
- name: MYSQL_ROOT_PASSWORD
|
||||
value: root
|
||||
---
|
||||
#
|
||||
# Frontend deployment
|
||||
#
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: fruitapp-frontend-deployment
|
||||
namespace: suhailahamed
|
||||
labels:
|
||||
app: fruitapp-frontend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: fruitapp-frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: fruitapp-frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: fruitapp-frontend
|
||||
image: fruitapp-frontend:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 80
|
||||
volumeMounts:
|
||||
- mountPath: /etc/nginx/conf.d/ # mount nginx-conf volumn to /etc/nginx
|
||||
readOnly: true
|
||||
name: nginx-conf
|
||||
volumes:
|
||||
- name: nginx-conf
|
||||
configMap:
|
||||
name: nginx-conf # place ConfigMap `nginx-conf` on /etc/nginx
|
||||
items:
|
||||
- key: default.conf
|
||||
path: default.conf
|
||||
---
|
||||
##
|
||||
## ConfigMap for nginx
|
||||
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: nginx-conf
|
||||
namespace: suhailahamed
|
||||
data:
|
||||
default.conf: |
|
||||
upstream Backend {
|
||||
# hello is the internal DNS name used by the backend Service inside Kubernetes
|
||||
server backend-service:5000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
location /api/ {
|
||||
# The following statement will proxy traffic to the upstream named Backend
|
||||
proxy_pass http://Backend;
|
||||
}
|
||||
}
|
4
z2/namespace.yaml
Normal file
4
z2/namespace.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: suhailahamed
|
5
z2/prepare-app.sh
Normal file
5
z2/prepare-app.sh
Normal file
@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
echo "Preparing the app.."
|
||||
docker build ./src/fruitapp-backend -t fruitapp-backend
|
||||
docker build ./src/fruitapp-frontend/ -f ./src/fruitapp-frontend/.docker/dev/Dockerfile -t fruitapp-frontend
|
||||
echo "Preparation complete."
|
27
z2/service.yml
Normal file
27
z2/service.yml
Normal file
@ -0,0 +1,27 @@
|
||||
# Backend service
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: backend-service
|
||||
namespace: suhailahamed
|
||||
spec:
|
||||
selector:
|
||||
app: fruitapp-backend
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5000
|
||||
targetPort: 5000
|
||||
---
|
||||
# Frontend service
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: frontend-service
|
||||
namespace: suhailahamed
|
||||
spec:
|
||||
selector:
|
||||
app: fruitapp-frontend
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
2
z2/src/.gitignore
vendored
Normal file
2
z2/src/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.history
|
||||
.idea
|
104
z2/src/Readme.md
Normal file
104
z2/src/Readme.md
Normal file
@ -0,0 +1,104 @@
|
||||
# Overview of the APP
|
||||
|
||||
The application consist of two parts, an API and a front end. The application displays 5 fruits which are seeded to the database at the startup. The get API provides the list of fruits. Anyone can like a fruit. In this case the count of like will be increased.
|
||||
|
||||
![homepage](images/homepage.png?raw=true "System Architecture")
|
||||
|
||||
# Requirements
|
||||
|
||||
The basic system requirements are as follows
|
||||
|
||||
- Any OS, preferably Linux
|
||||
- Docker
|
||||
- Docker compose
|
||||
|
||||
# Architecture
|
||||
|
||||
![Architecture](images/fruitapp.jpg?raw=true "System Architecture")
|
||||
|
||||
### Technology used
|
||||
|
||||
- Flask (Backend Rest API)
|
||||
- Angular (Frontend)
|
||||
- MySQL - as persistent database
|
||||
|
||||
# Docker environment
|
||||
|
||||
For shipping and deploying the application docker-compose is used. All the configurations are in the docker-compose.yml file.
|
||||
|
||||
Key points of the docker-compose.yml is given below.
|
||||
|
||||
## services
|
||||
|
||||
- **backend:** runs the Flask web API.
|
||||
- **db** runs the mysql database required for the backend API
|
||||
- **web** this is the front-end application
|
||||
|
||||
## Virtual networks
|
||||
|
||||
One virtual network is used
|
||||
|
||||
- main
|
||||
|
||||
## list of the containers
|
||||
|
||||
### fruitapp-backend
|
||||
|
||||
This container runs under the service name backend. The dockerfile user for this container is located at `fruitapp-backend/Dockerfile`. This container is based on python:3.9 image.
|
||||
|
||||
### fruitapp-frontend
|
||||
|
||||
The frontend container for the application. The dockerfile is located at `fruitapp-frontend/.docker/dev/Dockerfile`. Base image used to build this container is `Node:14`.
|
||||
|
||||
### db container
|
||||
|
||||
The container is built using the official `mysql:5.7.22` image pulled from dockerhub.
|
||||
|
||||
## Preparation
|
||||
|
||||
To prepare the environment for the first time run the following command
|
||||
|
||||
```shell
|
||||
bash prepare-app.sh
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
Run the app background
|
||||
|
||||
```shell
|
||||
bash start-app.sh
|
||||
```
|
||||
|
||||
see the logs of backend
|
||||
|
||||
```
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
see the logs of forntend
|
||||
|
||||
```
|
||||
docker-compose logs -f web
|
||||
```
|
||||
|
||||
see the logs of database
|
||||
|
||||
```
|
||||
docker-compose logs -f db
|
||||
```
|
||||
|
||||
## Stopping
|
||||
|
||||
```shell
|
||||
bash stop-app.sh
|
||||
```
|
||||
|
||||
## Removing containers
|
||||
|
||||
```shell
|
||||
bash remove-app.sh
|
||||
```
|
||||
# Viewing the app
|
||||
|
||||
After running the app. visit [http://localhost:4200](http://localhost:4200) to view the app
|
55
z2/src/docker-compose.yml
Normal file
55
z2/src/docker-compose.yml
Normal file
@ -0,0 +1,55 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./fruitapp-backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: fruitapp-backend
|
||||
env_file:
|
||||
- ./fruitapp-backend/.env
|
||||
command: "bash scripts/entrypoint.sh"
|
||||
ports:
|
||||
- "8001:5000"
|
||||
volumes:
|
||||
- ./fruitapp-backend:/app
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- main
|
||||
|
||||
db:
|
||||
image: mysql:5.7.38
|
||||
container_name: fruitapp-db
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./fruitapp-backend/.env
|
||||
volumes:
|
||||
- msql-data:/var/lib/mysql
|
||||
ports:
|
||||
- "33067:3306"
|
||||
networks:
|
||||
- main
|
||||
|
||||
web:
|
||||
build:
|
||||
context: ./fruitapp-frontend
|
||||
dockerfile: .docker/dev/Dockerfile
|
||||
container_name: fruitapp-frontend
|
||||
ports:
|
||||
- 4200:4200
|
||||
volumes:
|
||||
- ./fruitapp-frontend:/app
|
||||
command: >
|
||||
bash -c "cp -rfu /cache/node_modules/. /app/node_modules/ && ng serve --host=0.0.0.0 --aot"
|
||||
depends_on:
|
||||
- db
|
||||
- backend
|
||||
networks:
|
||||
- main
|
||||
|
||||
volumes:
|
||||
msql-data:
|
||||
|
||||
networks:
|
||||
main:
|
8
z2/src/fruitapp-backend/.env
Normal file
8
z2/src/fruitapp-backend/.env
Normal file
@ -0,0 +1,8 @@
|
||||
MYSQL_DATABASE=main
|
||||
MYSQL_USER=root
|
||||
MYSQL_PASSWORD=root
|
||||
MYSQL_ROOT_PASSWORD=root
|
||||
|
||||
SQLALCHEMY_DATABASE_URI=mysql://root:root@fruitapp-backend/main
|
||||
|
||||
FLASK_APP=app.py
|
6
z2/src/fruitapp-backend/.env.template
Normal file
6
z2/src/fruitapp-backend/.env.template
Normal file
@ -0,0 +1,6 @@
|
||||
MYSQL_DATABASE=main
|
||||
MYSQL_USER=root
|
||||
MYSQL_PASSWORD=root
|
||||
MYSQL_ROOT_PASSWORD=root
|
||||
|
||||
SQLALCHEMY_DATABASE_URI=mysql://root:root@test/main
|
23
z2/src/fruitapp-backend/.gitignore
vendored
Normal file
23
z2/src/fruitapp-backend/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
.flaskenv
|
||||
*.pyc
|
||||
*.pyo
|
||||
venv/
|
||||
.venv/
|
||||
dist/
|
||||
build/
|
||||
*.egg
|
||||
*.egg-info/
|
||||
_mailinglist
|
||||
.tox/
|
||||
.cache/
|
||||
.pytest_cache/
|
||||
.idea/
|
||||
docs/_build/
|
||||
.vscode
|
||||
|
||||
# Coverage reports
|
||||
htmlcov/
|
||||
.coverage
|
||||
.coverage.*
|
||||
*,cover
|
8
z2/src/fruitapp-backend/Dockerfile
Normal file
8
z2/src/fruitapp-backend/Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
FROM python:3.9
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
WORKDIR /app
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY ./scripts/entrypoint.sh /app/scripts/entrypoint.sh
|
||||
COPY . /app
|
2
z2/src/fruitapp-backend/config.py
Normal file
2
z2/src/fruitapp-backend/config.py
Normal file
@ -0,0 +1,2 @@
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
54
z2/src/fruitapp-backend/main.py
Normal file
54
z2/src/fruitapp-backend/main.py
Normal file
@ -0,0 +1,54 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
from flask import Flask, jsonify, abort
|
||||
from flask_cors import CORS
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI')
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Product(db.Model):
|
||||
id: int
|
||||
title: str
|
||||
image: str
|
||||
likes: int
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=False)
|
||||
title = db.Column(db.String(200))
|
||||
image = db.Column(db.TEXT(20000000))
|
||||
likes = db.Column(db.Integer, default=0)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProductUser(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer)
|
||||
product_id = db.Column(db.Integer)
|
||||
|
||||
|
||||
@app.route('/api/products')
|
||||
def index():
|
||||
return jsonify(Product.query.all())
|
||||
|
||||
|
||||
@app.route('/api/products/<int:id>/like', methods=['POST'])
|
||||
def like(id):
|
||||
# define a static user id for demo
|
||||
try:
|
||||
product = Product.query.get(id)
|
||||
product.likes += 1
|
||||
db.session.commit()
|
||||
except:
|
||||
abort(400, 'bad request')
|
||||
|
||||
return jsonify(product)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0')
|
12
z2/src/fruitapp-backend/manage.py
Normal file
12
z2/src/fruitapp-backend/manage.py
Normal file
@ -0,0 +1,12 @@
|
||||
from flask_migrate import Migrate, MigrateCommand
|
||||
from flask_script import Manager
|
||||
|
||||
from main import app, db
|
||||
|
||||
migrate = Migrate(app, db, compare_type=True)
|
||||
|
||||
manager = Manager(app)
|
||||
manager.add_command('db', MigrateCommand)
|
||||
|
||||
if __name__ == '__main__':
|
||||
manager.run()
|
1
z2/src/fruitapp-backend/migrations/README
Normal file
1
z2/src/fruitapp-backend/migrations/README
Normal file
@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
45
z2/src/fruitapp-backend/migrations/alembic.ini
Normal file
45
z2/src/fruitapp-backend/migrations/alembic.ini
Normal file
@ -0,0 +1,45 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
96
z2/src/fruitapp-backend/migrations/env.py
Normal file
96
z2/src/fruitapp-backend/migrations/env.py
Normal file
@ -0,0 +1,96 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from flask import current_app
|
||||
config.set_main_option(
|
||||
'sqlalchemy.url',
|
||||
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
24
z2/src/fruitapp-backend/migrations/script.py.mako
Normal file
24
z2/src/fruitapp-backend/migrations/script.py.mako
Normal file
@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
40
z2/src/fruitapp-backend/migrations/versions/843c810aec1f_.py
Normal file
40
z2/src/fruitapp-backend/migrations/versions/843c810aec1f_.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 843c810aec1f
|
||||
Revises:
|
||||
Create Date: 2020-12-08 08:40:37.736465
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '843c810aec1f'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('product',
|
||||
sa.Column('id', sa.Integer(), autoincrement=False, nullable=False),
|
||||
sa.Column('title', sa.String(length=200), nullable=True),
|
||||
sa.Column('image', sa.String(length=200), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('product_user',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('product_id', sa.Integer(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('product_user')
|
||||
op.drop_table('product')
|
||||
# ### end Alembic commands ###
|
34
z2/src/fruitapp-backend/migrations/versions/85749c0c4589_.py
Normal file
34
z2/src/fruitapp-backend/migrations/versions/85749c0c4589_.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 85749c0c4589
|
||||
Revises: b3ff59df2833
|
||||
Create Date: 2022-04-08 07:46:57.160792
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '85749c0c4589'
|
||||
down_revision = 'b3ff59df2833'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('product', 'image',
|
||||
existing_type=mysql.VARCHAR(length=20000),
|
||||
type_=sa.TEXT(length=20000000),
|
||||
existing_nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('product', 'image',
|
||||
existing_type=sa.TEXT(length=20000000),
|
||||
type_=mysql.VARCHAR(length=20000),
|
||||
existing_nullable=True)
|
||||
# ### end Alembic commands ###
|
34
z2/src/fruitapp-backend/migrations/versions/b3ff59df2833_.py
Normal file
34
z2/src/fruitapp-backend/migrations/versions/b3ff59df2833_.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: b3ff59df2833
|
||||
Revises: fee4d1b1d192
|
||||
Create Date: 2022-04-08 07:33:52.082355
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b3ff59df2833'
|
||||
down_revision = 'fee4d1b1d192'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('product', 'image',
|
||||
existing_type=mysql.VARCHAR(length=200),
|
||||
type_=sa.String(length=20000),
|
||||
existing_nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('product', 'image',
|
||||
existing_type=sa.String(length=20000),
|
||||
type_=mysql.VARCHAR(length=200),
|
||||
existing_nullable=True)
|
||||
# ### end Alembic commands ###
|
28
z2/src/fruitapp-backend/migrations/versions/fee4d1b1d192_.py
Normal file
28
z2/src/fruitapp-backend/migrations/versions/fee4d1b1d192_.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: fee4d1b1d192
|
||||
Revises: 843c810aec1f
|
||||
Create Date: 2022-04-08 07:05:04.647143
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'fee4d1b1d192'
|
||||
down_revision = '843c810aec1f'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('product', sa.Column('likes', sa.Integer(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('product', 'likes')
|
||||
# ### end Alembic commands ###
|
27
z2/src/fruitapp-backend/producer.py
Normal file
27
z2/src/fruitapp-backend/producer.py
Normal file
@ -0,0 +1,27 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import pika
|
||||
|
||||
RABBIT_ENDPOINT = os.environ['RABBIT_ENDPOINT']
|
||||
|
||||
|
||||
class Producer:
|
||||
"""Implement the producer logic."""
|
||||
|
||||
def __init__(self):
|
||||
self.params = pika.URLParameters(RABBIT_ENDPOINT)
|
||||
self.connection = pika.BlockingConnection(self.params)
|
||||
|
||||
def publish(self, method, body):
|
||||
properties = pika.BasicProperties(method)
|
||||
if not self.connection or self.connection.is_closed:
|
||||
self.connection = pika.BlockingConnection(self.params)
|
||||
channel = self.connection.channel()
|
||||
channel.basic_publish(
|
||||
exchange='',
|
||||
routing_key='admin',
|
||||
body=json.dumps(body),
|
||||
properties=properties,
|
||||
)
|
||||
print('Product published in queue: MAIN')
|
13
z2/src/fruitapp-backend/requirements.txt
Normal file
13
z2/src/fruitapp-backend/requirements.txt
Normal file
@ -0,0 +1,13 @@
|
||||
Flask==1.1.2
|
||||
werkzeug==1.0.1
|
||||
Flask-SQLAlchemy==2.4.4
|
||||
SQLAlchemy==1.3.20
|
||||
Flask-Migrate==2.5.3
|
||||
Flask-Script==2.0.6
|
||||
Flask-Cors==3.0.9
|
||||
requests==2.25.0
|
||||
mysqlclient==2.0.1
|
||||
pika==1.1.0
|
||||
jinja2<3.1.0
|
||||
itsdangerous==2.0.1
|
||||
python-dotenv==0.20.0
|
5
z2/src/fruitapp-backend/scripts/entrypoint.sh
Normal file
5
z2/src/fruitapp-backend/scripts/entrypoint.sh
Normal file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
python manage.py db upgrade
|
||||
echo "DB migration done."
|
||||
python seeder.py
|
||||
python main.py
|
40
z2/src/fruitapp-backend/seeder.py
Normal file
40
z2/src/fruitapp-backend/seeder.py
Normal file
@ -0,0 +1,40 @@
|
||||
from main import Product, db
|
||||
|
||||
|
||||
def insert_data(data):
|
||||
product = Product.query.get(data['id'])
|
||||
if not product:
|
||||
product = Product(id=data['id'], title=data['title'], image=data['image'])
|
||||
db.session.add(product)
|
||||
db.session.commit()
|
||||
print('inserted ', data['title'])
|
||||
else:
|
||||
print('Product already exists.')
|
||||
|
||||
|
||||
PRODUCTS = [
|
||||
{
|
||||
'id': 1,
|
||||
'title': 'Apple',
|
||||
'image': 'https://media.istockphoto.com/photos/red-apple-picture-id184276818?k=20&m=184276818&s=612x612&w=0&h=QxOcueqAUVTdiJ7DVoCu-BkNCIuwliPEgtAQhgvBA_g='
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'title': 'Orange',
|
||||
'image': 'https://media.istockphoto.com/photos/tiny-tangerine-picture-id89951356?k=20&m=89951356&s=170667a&w=0&h=9oW2EBTASfaBol8qYVYB8YNYcSoNe1AImG4wqdgErEs='
|
||||
},
|
||||
{
|
||||
'id': 3,
|
||||
'title': 'Banana',
|
||||
'image': 'https://media.istockphoto.com/photos/banana-picture-id1184345169?k=20&m=1184345169&s=170667a&w=0&h=t22KqOZ9EEwyRj7i35uxY-Xf6P_gAgLejd-SeReTnPY='
|
||||
},
|
||||
{
|
||||
'id': 4,
|
||||
'title': 'Avocado',
|
||||
'image': 'https://media.istockphoto.com/photos/half-of-fresh-ripe-avocado-isolated-on-white-background-picture-id1278032327?b=1&k=20&m=1278032327&s=170667a&w=0&h=7eMAI9S_YBiVaDN49zk5gUZEhhA1oGDIWh3Tdst6q4Y='
|
||||
}
|
||||
]
|
||||
|
||||
for p in PRODUCTS:
|
||||
insert_data(p)
|
||||
|
17
z2/src/fruitapp-frontend/.browserslistrc
Normal file
17
z2/src/fruitapp-frontend/.browserslistrc
Normal file
@ -0,0 +1,17 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# For the full list of supported browsers by the Angular framework, please see:
|
||||
# https://angular.io/guide/browser-support
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
28
z2/src/fruitapp-frontend/.docker/dev/Dockerfile
Normal file
28
z2/src/fruitapp-frontend/.docker/dev/Dockerfile
Normal file
@ -0,0 +1,28 @@
|
||||
# Stage 1: Compile and Build angular codebase
|
||||
|
||||
# Use official node image as the base image
|
||||
FROM node:latest as build
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /usr/local/app
|
||||
ENV NODE_OPTIONS=--openssl-legacy-provider
|
||||
# Add the source code to app
|
||||
COPY ./ /usr/local/app/
|
||||
|
||||
# Install all the dependencies
|
||||
RUN npm install --legacy-peer-deps
|
||||
|
||||
# Generate the build of the application
|
||||
RUN npm run build
|
||||
|
||||
|
||||
# Stage 2: Serve app with nginx server
|
||||
|
||||
# Use official nginx image as the base image
|
||||
FROM nginx:latest
|
||||
|
||||
# Copy the build output to replace the default nginx contents.
|
||||
COPY --from=build /usr/local/app/dist/dockerize-angular /usr/share/nginx/html
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
9
z2/src/fruitapp-frontend/.dockerignore
Normal file
9
z2/src/fruitapp-frontend/.dockerignore
Normal file
@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
.vscode
|
||||
.history
|
16
z2/src/fruitapp-frontend/.editorconfig
Normal file
16
z2/src/fruitapp-frontend/.editorconfig
Normal file
@ -0,0 +1,16 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
46
z2/src/fruitapp-frontend/.gitignore
vendored
Normal file
46
z2/src/fruitapp-frontend/.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
27
z2/src/fruitapp-frontend/README.md
Normal file
27
z2/src/fruitapp-frontend/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# DockerizeAngular
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 11.2.2.
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
130
z2/src/fruitapp-frontend/angular.json
Normal file
130
z2/src/fruitapp-frontend/angular.json
Normal file
@ -0,0 +1,130 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"cli": {
|
||||
"analytics": "d44c8c58-a3f9-4508-9e41-039ac5874480"
|
||||
},
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"dockerize-angular": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/dockerize-angular",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [ "./node_modules/@angular/material/prebuilt-themes/purple-green.css"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "dockerize-angular:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "dockerize-angular:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "dockerize-angular:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"./node_modules/@angular/material/prebuilt-themes/purple-green.css",
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "dockerize-angular:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "dockerize-angular:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "dockerize-angular"
|
||||
}
|
37
z2/src/fruitapp-frontend/e2e/protractor.conf.js
Normal file
37
z2/src/fruitapp-frontend/e2e/protractor.conf.js
Normal file
@ -0,0 +1,37 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
browserName: 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
SELENIUM_PROMISE_MANAGER: false,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({
|
||||
spec: {
|
||||
displayStacktrace: StacktraceOption.PRETTY
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
23
z2/src/fruitapp-frontend/e2e/src/app.e2e-spec.ts
Normal file
23
z2/src/fruitapp-frontend/e2e/src/app.e2e-spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { browser, logging } from 'protractor';
|
||||
import { AppPage } from './app.po';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', async () => {
|
||||
await page.navigateTo();
|
||||
expect(await page.getTitleText()).toEqual('dockerize-angular app is running!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
11
z2/src/fruitapp-frontend/e2e/src/app.po.ts
Normal file
11
z2/src/fruitapp-frontend/e2e/src/app.po.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
async navigateTo(): Promise<unknown> {
|
||||
return browser.get(browser.baseUrl);
|
||||
}
|
||||
|
||||
async getTitleText(): Promise<string> {
|
||||
return element(by.css('app-root .content span')).getText();
|
||||
}
|
||||
}
|
13
z2/src/fruitapp-frontend/e2e/tsconfig.json
Normal file
13
z2/src/fruitapp-frontend/e2e/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
57
z2/src/fruitapp-frontend/karma.conf.js
Normal file
57
z2/src/fruitapp-frontend/karma.conf.js
Normal file
@ -0,0 +1,57 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
customLaunchers: {
|
||||
ChromeHeadless: {
|
||||
base: 'Chrome',
|
||||
flags: [
|
||||
'--no-sandbox',
|
||||
'--disable-gpu',
|
||||
'--headless',
|
||||
'--remote-debugging-port=9222'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/dockerize-angular'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
]
|
||||
},
|
||||
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
30732
z2/src/fruitapp-frontend/package-lock.json
generated
Normal file
30732
z2/src/fruitapp-frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
z2/src/fruitapp-frontend/package.json
Normal file
49
z2/src/fruitapp-frontend/package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "dockerize-angular",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~11.2.3",
|
||||
"@angular/cdk": "^11.2.13",
|
||||
"@angular/common": "~11.2.3",
|
||||
"@angular/compiler": "~11.2.3",
|
||||
"@angular/core": "~11.2.3",
|
||||
"@angular/forms": "~11.2.3",
|
||||
"@angular/localize": "~11.2.3",
|
||||
"@angular/material": "^11.2.13",
|
||||
"@angular/platform-browser": "~11.2.3",
|
||||
"@angular/platform-browser-dynamic": "~11.2.3",
|
||||
"@angular/router": "~11.2.3",
|
||||
"hello-world-npm": "^1.1.1",
|
||||
"rxjs": "~6.6.0",
|
||||
"tslib": "^2.0.0",
|
||||
"zone.js": "~0.11.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.1102.2",
|
||||
"@angular/cli": "~11.2.2",
|
||||
"@angular/compiler-cli": "~11.2.3",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^6.0.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~6.1.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.0.3",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "~4.1.2"
|
||||
}
|
||||
}
|
14
z2/src/fruitapp-frontend/src/app/app-routing.module.ts
Normal file
14
z2/src/fruitapp-frontend/src/app/app-routing.module.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
|
||||
{ path: 'dashboard', component: DashboardComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
0
z2/src/fruitapp-frontend/src/app/app.component.css
Normal file
0
z2/src/fruitapp-frontend/src/app/app.component.css
Normal file
1
z2/src/fruitapp-frontend/src/app/app.component.html
Normal file
1
z2/src/fruitapp-frontend/src/app/app.component.html
Normal file
@ -0,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
35
z2/src/fruitapp-frontend/src/app/app.component.spec.ts
Normal file
35
z2/src/fruitapp-frontend/src/app/app.component.spec.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'dockerize-angular'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('dockerize-angular');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('.content span').textContent).toContain('dockerize-angular app is running!');
|
||||
});
|
||||
});
|
10
z2/src/fruitapp-frontend/src/app/app.component.ts
Normal file
10
z2/src/fruitapp-frontend/src/app/app.component.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'dockerize-angular';
|
||||
}
|
35
z2/src/fruitapp-frontend/src/app/app.module.ts
Normal file
35
z2/src/fruitapp-frontend/src/app/app.module.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { HttpErrorHandler } from './service/http-error-handler.service';
|
||||
import { MessageService } from './service/message.service';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
DashboardComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
HttpClientModule,
|
||||
BrowserAnimationsModule,
|
||||
MatInputModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatCardModule
|
||||
],
|
||||
providers: [
|
||||
HttpErrorHandler,
|
||||
MessageService,
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
@ -0,0 +1,21 @@
|
||||
.product-card-wrapper{
|
||||
width: 1112px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card{
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mat-card{
|
||||
background: #fff;
|
||||
color: #424242;
|
||||
}
|
||||
|
||||
.example-card {
|
||||
max-width: 200px;
|
||||
margin-top: 20px;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<div class="product-card-wrapper">
|
||||
<div class="card" *ngFor="let p of products">
|
||||
<mat-card class="example-card">
|
||||
<mat-card-title style="margin-bottom: 15px; font-size: 18px;">{{ p.title }}</mat-card-title>
|
||||
<mat-card-content style="margin-bottom: 0;">
|
||||
<img src="{{ p.image }}" height="180" />
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-button (click)="likeProduct(p.id)" style="display: flex; align-items: center;">
|
||||
<mat-icon aria-hidden="false" aria-label="Example home icon">thumb_up</mat-icon>
|
||||
<span style="margin-left: 10px;">{{ p.likes }}</span>
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
|
||||
describe('DashboardComponent', () => {
|
||||
let component: DashboardComponent;
|
||||
let fixture: ComponentFixture<DashboardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DashboardComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DashboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,56 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { Product } from '../models/product';
|
||||
import { DashboardService } from '../service/dashboard.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrls: ['./dashboard.component.css']
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
|
||||
products: Product[] = [];
|
||||
constructor(private dashboardService: DashboardService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
console.log('dashboard');
|
||||
this.getProducts();
|
||||
|
||||
}
|
||||
|
||||
getProducts() {
|
||||
this.dashboardService.getProducts()
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
data => {
|
||||
this.products = data;
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
likeProduct(id: number) {
|
||||
console.log('like clicked ...........');
|
||||
this.dashboardService.likeProduct(id)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
data => {
|
||||
this.products.forEach(product => {
|
||||
if (product.id == data.id) {
|
||||
product.likes = data.likes;
|
||||
}
|
||||
});
|
||||
console.log(data);
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
6
z2/src/fruitapp-frontend/src/app/models/product.ts
Normal file
6
z2/src/fruitapp-frontend/src/app/models/product.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export class Product {
|
||||
id!: number;
|
||||
title!: string;
|
||||
image!: string;
|
||||
likes!: number;
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { HandleError, HttpErrorHandler } from './http-error-handler.service';
|
||||
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DashboardService {
|
||||
|
||||
apiUrl = environment.apiUrl;
|
||||
private handleError: HandleError;
|
||||
|
||||
constructor(private http: HttpClient, httpErrorHandler: HttpErrorHandler) {
|
||||
this.handleError = httpErrorHandler.createHandleError('DashboardService');
|
||||
}
|
||||
|
||||
getProducts(): Observable<any> {
|
||||
return this.http.get<any>(this.apiUrl + '/api/products')
|
||||
.pipe(
|
||||
catchError(this.handleError('getToDo', []))
|
||||
);
|
||||
}
|
||||
|
||||
likeProduct(id: number): Observable<any> {
|
||||
return this.http.post<any>(this.apiUrl + `/api/products/${id}/like`, null)
|
||||
.pipe(
|
||||
catchError(this.handleError('like'))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { MessageService } from './message.service';
|
||||
|
||||
|
||||
|
||||
/** Type of the handleError function returned by HttpErrorHandler.createHandleError */
|
||||
export type HandleError =
|
||||
<T> (operation?: string, result?: T) => (error: HttpErrorResponse) => Observable<T>;
|
||||
|
||||
/** Handles HttpClient errors */
|
||||
@Injectable()
|
||||
export class HttpErrorHandler {
|
||||
constructor(private messageService: MessageService) {
|
||||
}
|
||||
|
||||
/** Create curried handleError function that already knows the service name */
|
||||
createHandleError = (serviceName = '') => <T>
|
||||
(operation = 'operation', result = {} as T) => this.handleError(serviceName, operation, result);
|
||||
|
||||
/**
|
||||
* Returns a function that handles Http operation failures.
|
||||
* This error handler lets the app continue to run as if no error occurred.
|
||||
* @param serviceName = name of the data service that attempted the operation
|
||||
* @param operation - name of the operation that failed
|
||||
* @param result - optional value to return as the observable result
|
||||
*/
|
||||
handleError<T>(serviceName = '', operation = 'operation', result = {} as T) {
|
||||
|
||||
return (error: HttpErrorResponse): Observable<T> => {
|
||||
// TODO: send the error to remote logging infrastructure
|
||||
console.log(error); // log to console instead
|
||||
const message = (error.error instanceof ErrorEvent) ?
|
||||
error.error.message :
|
||||
error.message;
|
||||
|
||||
// TODO: better job of transforming error for user consumption
|
||||
this.messageService.add(message);
|
||||
|
||||
// Let the app keep running by returning a safe result.
|
||||
return throwError(error);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
17
z2/src/fruitapp-frontend/src/app/service/message.service.ts
Normal file
17
z2/src/fruitapp-frontend/src/app/service/message.service.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class MessageService {
|
||||
messages: string[] = [];
|
||||
messageType = '';
|
||||
|
||||
add(message: string) {
|
||||
if (message && message !== '') {
|
||||
this.messages.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.messages = [];
|
||||
}
|
||||
}
|
0
z2/src/fruitapp-frontend/src/assets/.gitkeep
Normal file
0
z2/src/fruitapp-frontend/src/assets/.gitkeep
Normal file
@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
production: true
|
||||
};
|
17
z2/src/fruitapp-frontend/src/environments/environment.ts
Normal file
17
z2/src/fruitapp-frontend/src/environments/environment.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiUrl: ""
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
BIN
z2/src/fruitapp-frontend/src/favicon.ico
Normal file
BIN
z2/src/fruitapp-frontend/src/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 948 B |
16
z2/src/fruitapp-frontend/src/index.html
Normal file
16
z2/src/fruitapp-frontend/src/index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Main</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body class="mat-typography">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
12
z2/src/fruitapp-frontend/src/main.ts
Normal file
12
z2/src/fruitapp-frontend/src/main.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
69
z2/src/fruitapp-frontend/src/polyfills.ts
Normal file
69
z2/src/fruitapp-frontend/src/polyfills.ts
Normal file
@ -0,0 +1,69 @@
|
||||
/***************************************************************************************************
|
||||
* Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
|
||||
*/
|
||||
import '@angular/localize/init';
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/**
|
||||
* IE11 requires the following for NgClass support on SVG elements
|
||||
*/
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
2
z2/src/fruitapp-frontend/src/styles.css
Normal file
2
z2/src/fruitapp-frontend/src/styles.css
Normal file
@ -0,0 +1,2 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
|
25
z2/src/fruitapp-frontend/src/test.ts
Normal file
25
z2/src/fruitapp-frontend/src/test.ts
Normal file
@ -0,0 +1,25 @@
|
||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/dist/zone-testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
declare const require: {
|
||||
context(path: string, deep?: boolean, filter?: RegExp): {
|
||||
keys(): string[];
|
||||
<T>(id: string): T;
|
||||
};
|
||||
};
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting()
|
||||
);
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().map(context);
|
15
z2/src/fruitapp-frontend/tsconfig.app.json
Normal file
15
z2/src/fruitapp-frontend/tsconfig.app.json
Normal file
@ -0,0 +1,15 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
32
z2/src/fruitapp-frontend/tsconfig.json
Normal file
32
z2/src/fruitapp-frontend/tsconfig.json
Normal file
@ -0,0 +1,32 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"module": "es2020",
|
||||
"lib": [
|
||||
"es2018",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"strictPropertyInitialization": false
|
||||
|
||||
}
|
18
z2/src/fruitapp-frontend/tsconfig.spec.json
Normal file
18
z2/src/fruitapp-frontend/tsconfig.spec.json
Normal file
@ -0,0 +1,18 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/test.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
152
z2/src/fruitapp-frontend/tslint.json
Normal file
152
z2/src/fruitapp-frontend/tslint.json
Normal file
@ -0,0 +1,152 @@
|
||||
{
|
||||
"extends": "tslint:recommended",
|
||||
"rulesDirectory": [
|
||||
"codelyzer"
|
||||
],
|
||||
"rules": {
|
||||
"align": {
|
||||
"options": [
|
||||
"parameters",
|
||||
"statements"
|
||||
]
|
||||
},
|
||||
"array-type": false,
|
||||
"arrow-return-shorthand": true,
|
||||
"curly": true,
|
||||
"deprecation": {
|
||||
"severity": "warning"
|
||||
},
|
||||
"eofline": true,
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs/Rx"
|
||||
],
|
||||
"import-spacing": true,
|
||||
"indent": {
|
||||
"options": [
|
||||
"spaces"
|
||||
]
|
||||
},
|
||||
"max-classes-per-file": false,
|
||||
"max-line-length": [
|
||||
true,
|
||||
140
|
||||
],
|
||||
"member-ordering": [
|
||||
true,
|
||||
{
|
||||
"order": [
|
||||
"static-field",
|
||||
"instance-field",
|
||||
"static-method",
|
||||
"instance-method"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-console": [
|
||||
true,
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-empty": false,
|
||||
"no-inferrable-types": [
|
||||
true,
|
||||
"ignore-params"
|
||||
],
|
||||
"no-non-null-assertion": true,
|
||||
"no-redundant-jsdoc": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-var-requires": false,
|
||||
"object-literal-key-quotes": [
|
||||
true,
|
||||
"as-needed"
|
||||
],
|
||||
"quotemark": [
|
||||
true,
|
||||
"single"
|
||||
],
|
||||
"semicolon": {
|
||||
"options": [
|
||||
"always"
|
||||
]
|
||||
},
|
||||
"space-before-function-paren": {
|
||||
"options": {
|
||||
"anonymous": "never",
|
||||
"asyncArrow": "always",
|
||||
"constructor": "never",
|
||||
"method": "never",
|
||||
"named": "never"
|
||||
}
|
||||
},
|
||||
"typedef": [
|
||||
true,
|
||||
"call-signature"
|
||||
],
|
||||
"typedef-whitespace": {
|
||||
"options": [
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
},
|
||||
{
|
||||
"call-signature": "onespace",
|
||||
"index-signature": "onespace",
|
||||
"parameter": "onespace",
|
||||
"property-declaration": "onespace",
|
||||
"variable-declaration": "onespace"
|
||||
}
|
||||
]
|
||||
},
|
||||
"variable-name": {
|
||||
"options": [
|
||||
"ban-keywords",
|
||||
"check-format",
|
||||
"allow-pascal-case"
|
||||
]
|
||||
},
|
||||
"whitespace": {
|
||||
"options": [
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type",
|
||||
"check-typecast"
|
||||
]
|
||||
},
|
||||
"component-class-suffix": true,
|
||||
"contextual-lifecycle": true,
|
||||
"directive-class-suffix": true,
|
||||
"no-conflicting-lifecycle": true,
|
||||
"no-host-metadata-property": true,
|
||||
"no-input-rename": true,
|
||||
"no-inputs-metadata-property": true,
|
||||
"no-output-native": true,
|
||||
"no-output-on-prefix": true,
|
||||
"no-output-rename": true,
|
||||
"no-outputs-metadata-property": true,
|
||||
"template-banana-in-box": true,
|
||||
"template-no-negated-async": true,
|
||||
"use-lifecycle-interface": true,
|
||||
"use-pipe-transform-interface": true,
|
||||
"directive-selector": [
|
||||
true,
|
||||
"attribute",
|
||||
"app",
|
||||
"camelCase"
|
||||
],
|
||||
"component-selector": [
|
||||
true,
|
||||
"element",
|
||||
"app",
|
||||
"kebab-case"
|
||||
]
|
||||
}
|
||||
}
|
BIN
z2/src/images/fruitapp.jpg
Normal file
BIN
z2/src/images/fruitapp.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
z2/src/images/homepage.png
Normal file
BIN
z2/src/images/homepage.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 209 KiB |
5
z2/src/prepare-app.sh
Normal file
5
z2/src/prepare-app.sh
Normal file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Preparing the app.."
|
||||
docker-compose build
|
||||
echo "Preparation complete."
|
14
z2/src/remove-app.sh
Normal file
14
z2/src/remove-app.sh
Normal file
@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Removing the app.."
|
||||
echo "Removing the fruitapp-backend"
|
||||
docker stop fruitapp-backend && docker rm -f fruitapp-backend
|
||||
echo ""
|
||||
echo "removing the fruitapp-web container"
|
||||
docker stop fruitapp-frontend && docker rm -f fruitapp-frontend
|
||||
|
||||
echo ""
|
||||
echo "removing the fruitapp-db container"
|
||||
docker stop fruitapp-db && docker rm -f fruitapp-db
|
||||
|
||||
echo "App removed."
|
7
z2/src/start-app.sh
Normal file
7
z2/src/start-app.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "running the app"
|
||||
docker-compose up -d
|
||||
echo "Connecting to the database....Please wait."
|
||||
sleep 10
|
||||
echo "The app is available at http://localhost:4200"
|
5
z2/src/stop-app.sh
Normal file
5
z2/src/stop-app.sh
Normal file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "stopping the app.."
|
||||
docker-compose stop
|
||||
echo "App stopped"
|
9
z2/start-app.sh
Normal file
9
z2/start-app.sh
Normal file
@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
echo 'starting the app'
|
||||
kubectl apply -f namespace.yaml
|
||||
kubectl apply -f statefulset.yml
|
||||
kubectl apply -f statefulset-service.yml
|
||||
kubectl create -f deployment.yml
|
||||
kubectl create -f service.yml
|
||||
kubectl create -f ingress.yml
|
||||
echo 'app started'
|
15
z2/statefulset-service.yml
Normal file
15
z2/statefulset-service.yml
Normal file
@ -0,0 +1,15 @@
|
||||
# MySQL StatefulSet Service
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mysql-service
|
||||
namespace: suhailahamed
|
||||
spec:
|
||||
selector:
|
||||
app: mysql
|
||||
type: ClusterIP
|
||||
clusterIP: None
|
||||
ports:
|
||||
- port: 3306
|
||||
targetPort: mysql
|
||||
protocol: TCP
|
63
z2/statefulset.yml
Normal file
63
z2/statefulset.yml
Normal file
@ -0,0 +1,63 @@
|
||||
# PostgreSQL StatefulSet
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: mysql
|
||||
namespace: suhailahamed
|
||||
spec:
|
||||
serviceName: mysql-service
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mysql
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mysql
|
||||
spec:
|
||||
containers:
|
||||
- name: mysql
|
||||
image: mysql:8.0
|
||||
imagePullPolicy: 'IfNotPresent'
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/mysql
|
||||
env:
|
||||
- name: MYSQL_DATABASE
|
||||
value: 'main'
|
||||
- name: MYSQL_USER
|
||||
value: 'user'
|
||||
- name: MYSQL_PASSWORD
|
||||
value: 'password'
|
||||
- name: MYSQL_ROOT_PASSWORD
|
||||
value: 'root1'
|
||||
|
||||
ports:
|
||||
- name: mysql
|
||||
containerPort: 3306
|
||||
protocol: TCP
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
namespace: suhailahamed
|
||||
spec:
|
||||
storageClassName: ''
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: pv-volume
|
||||
labels:
|
||||
type: local
|
||||
spec:
|
||||
storageClassName: ''
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
accessModes: ['ReadWriteOnce']
|
||||
hostPath:
|
||||
path: '/mnt/pg'
|
4
z2/stop-app.sh
Normal file
4
z2/stop-app.sh
Normal file
@ -0,0 +1,4 @@
|
||||
echo 'Deleting namespace'
|
||||
kubectl delete namespaces suhailahamed
|
||||
echo 'Deleting persistent volume'
|
||||
kubectl delete pv pv-volume
|
Loading…
Reference in New Issue
Block a user