Add v4, with proxy, app and db

This commit is contained in:
Elton Stoneman 2017-11-20 17:00:54 +00:00
parent 3f2894bea0
commit 1e92429385
19 changed files with 463 additions and 0 deletions

2
bulletin-board-app/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.DS_Store
node_modules

View File

@ -0,0 +1,10 @@
FROM node:6.11.5
WORKDIR /usr/src/app
COPY package.json .
RUN npm install
EXPOSE 8080
CMD [ "npm", "start" ]
COPY . .

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Ryan Chenkie <ryan@elevatedigital.io> (http://elevatedigital.io)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

54
bulletin-board-app/app.js Normal file
View File

@ -0,0 +1,54 @@
new Vue({
el: '#events',
data: {
event: { title: '', detail: '', date: '' },
events: []
},
ready: function () {
this.fetchEvents();
},
methods: {
fetchEvents: function () {
var events = [];
this.$http.get('/api/events')
.success(function (events) {
this.$set('events', events);
console.log(events);
})
.error(function (err) {
console.log(err);
});
},
addEvent: function () {
if (this.event.title.trim()) {
this.$http.post('/api/events', this.event)
.success(function (res) {
this.events.push(this.event);
console.log('Event added!');
})
.error(function (err) {
console.log(err);
});
}
},
deleteEvent: function (id) {
if (confirm('Are you sure you want to delete this event?')) {
this.$http.delete('api/events/' + id)
.success(function (res) {
console.log(res);
var index = this.events.find(x => x.id === id)
this.events.splice(index, 1);
})
.error(function (err) {
console.log(err);
});
}
}
}
});

View File

@ -0,0 +1,43 @@
var db = require('./db.js');
exports.events = function (req, res) {
console.log('Loading DB events...');
db.Events
.findAll()
.then(events => {
console.log('Fetched events, count: ' + events.length);
res.json(events);
})
.catch(err => {
console.error('** Fetch failed: ', err);
});
};
exports.event = function (req, res) {
console.log('Handling event call, method: ' + req.method + ', event ID: ' + req.params.eventId)
switch(req.method) {
case "DELETE":
db.Events
.destroy({
where: {
id: req.params.eventId
}
}).then(function() {
console.log('Deleted event with id: ' + req.params.eventId)
res.status(200).end();
});
break
case "POST":
db.Events
.create({
title: req.body.title,
detail: req.body.detail,
date: req.body.date
})
.then(function() {
res.send('{}');
res.status(201).end();
});
break
}
};

View File

@ -0,0 +1,34 @@
var Sequelize = require('sequelize');
var username = 'sa';
var password = 'DockerCon!!!';
var host = 'bb-db';
var dbName = 'BulletinBoard';
var sequelize = new Sequelize(dbName, username, password, {
dialect: 'mssql',
host: host,
port: 1433,
dialectOptions: {
requestTimeout: 30000
}
});
sequelize
.authenticate()
.then(() => {
console.log('Successful connection to SQL Server.');
})
.catch(err => {
console.error('** SQL Server connection failed: ', err);
process.exit(1);
});
var Event = sequelize.define('event', {
title: Sequelize.STRING,
detail: Sequelize.STRING,
date: Sequelize.DATE
});
Event.sync();
exports.Events = Event;

View File

@ -0,0 +1,3 @@
exports.index = function (req, res) {
res.render('index');
}

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Bulletin Board</title>
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="site.css">
<style>
.form-control {
margin-bottom: 10px;
}
.btn-danger {
margin-top: 8px;
}
</style>
</head>
<body>
<div class="jumbotron">
<h1>Welcome to the Bulletin Board</h1>
</div>
<div class="container" id="events">
<div class="col-sm-7">
<div class="panel panel-default">
<div class="panel-heading">
<h3>Add an Event</h3>
</div>
<div class="panel-body">
<div>
<input class="form-control" placeholder="Title" v-model="event.title">
<textarea class="form-control" placeholder="Detail" v-model="event.detail"></textarea>
<input type="date" class="form-control" placeholder="Date" v-model="event.date">
<button class="btn btn-primary" v-on:click="addEvent">Submit</button>
</div>
</div>
</div>
</div>
<div class="col-sm-5">
<div class="list-group">
<a href="#" class="list-group-item" v-for="event in events">
<h4 class="list-group-item-heading"><i class="glyphicon glyphicon-bullhorn"></i> {{ event.title }}</h4>
<h5><i class="glyphicon glyphicon-calendar" v-if="event.date"></i> {{ event.date }}</h5>
<p class="list-group-item-text" v-if="event.detail">{{ event.detail }}</p>
<button class="btn btn-xs btn-danger" v-on:click="deleteEvent(event.id)">Delete</button>
</a>
</div>
</div>
</div>
</body>
<script src="node_modules/vue/dist/vue.min.js"></script>
<script src="node_modules/vue-resource/dist/vue-resource.min.js"></script>
<script src="app.js"></script>
</html>

View File

@ -0,0 +1,24 @@
{
"name": "vue-event-bulletin",
"version": "1.0.0",
"description": "Demo application for the scotch.io tutorial",
"main": "server.js",
"author": "Ryan Chenkie, Jason Lam",
"license": "MIT",
"dependencies": {
"bootstrap": "^3.3.6",
"ejs": "^2.3.4",
"express": "^4.13.3",
"morgan": "^1.6.1",
"vue": "^1.0.10",
"vue-resource": "^0.1.17",
"tedious": "^2.0.1",
"sequelize": "^4.20.1"
},
"devDependencies": {
"body-parser": "^1.14.1",
"errorhandler": "^1.4.2",
"method-override": "^2.3.5",
"morgan": "^1.6.1"
}
}

View File

@ -0,0 +1,20 @@
## Vue Events Bulletin Board
This is the code for the Vue.js [tutorial on Scotch.io](https://scotch.io/tutorials/build-a-single-page-time-tracking-app-with-vue-js-introduction). In the tutorial we build a events bulletin board application and cover the basics of [Vue](http://vuejs.org/).
## Installation
1. Run `npm install`.
2. Run `node server.js`.
3. Visit [http://localhost:8080](http://localhost:8080).
## RESTful API (contributed by Jason Lam)
1. **Use Node.js & Express for backend server and router.**
2. **RESTful requests towards the server to simulate CRUD on *events* model, instead of local hardcoded ones.**
3. Translated into Traditional Chinese.
## RESTful API written in Go
If you would like to use a backend written in Go, [thewhitetulip](http://github.com/thewhitetulip) has written on. See [the source code](https://github.com/thewhitetulip/go-vue-events).

View File

@ -0,0 +1,38 @@
var express = require('express'),
bodyParser = require('body-parser'),
methodOverride = require('method-override'),
errorHandler = require('errorhandler'),
morgan = require('morgan'),
routes = require('./backend'),
api = require('./backend/api');
var app = module.exports = express();
app.engine('html', require('ejs').renderFile);
app.set('view engine', 'html');
app.use(morgan('dev'));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(methodOverride());
app.use(express.static(__dirname + '/'));
app.use('/build', express.static('public'));
var env = process.env.NODE_ENV;
if ('development' == env) {
app.use(errorHandler({
dumpExceptions: true,
showStack: true
}));
}
if ('production' == app.get('env')) {
app.use(errorHandler());
}
app.get('/', routes.index);
app.get('/api/events', api.events);
app.post('/api/events', api.event);
app.delete('/api/events/:eventId', api.event)
app.listen(8080);
console.log('Magic happens on port 8080...');

View File

@ -0,0 +1,50 @@
/* v2 */
@font-face {
font-family: 'Geomanist Book';
src: url(fonts/geomanist/hinted-Geomanist-Book.eot);
src: url(fonts/geomanist/hinted-Geomanist-Book.eot?#iefix) format('embedded-opentype'),url(../fonts/geomanist/hinted-Geomanist-Book.woff2) format('woff2'),url(../fonts/geomanist/hinted-Geomanist-Book.woff) format('woff'),url(../fonts/geomanist/hinted-Geomanist-Book.ttf) format('truetype'),url(../fonts/geomanist/hinted-Geomanist-Book.svg#Geomanist-Book) format('svg');
font-weight: 400;
font-style: normal
}
body {
padding-bottom: 20px;
background-color: rgb(98, 41, 145);
color: #FFF;
font-size: 20px;
}
/* Wrapping element */
/* Set some basic padding to keep content from hitting the edges */
.body-content {
padding-left: 15px;
padding-right: 15px;
}
/* Set widths on the form inputs since otherwise they're 100% wide */
input,
select,
textarea {
max-width: 280px;
}
/* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) {
.body-content {
padding: 0;
}
}
/* v2 */
.jumbotron {
font-family: 'Geomanist Book';
background-color: #380a66;
}
h1, h2 {
font-family: 'Geomanist Book';
text-align: center;
}

View File

@ -0,0 +1,10 @@
FROM microsoft/mssql-server-linux:2017-CU1
ENV ACCEPT_EULA=Y \
MSSQL_SA_PASSWORD=DockerCon!!!
WORKDIR /init
COPY init-db.* ./
RUN chmod +x ./init-db.sh
RUN /opt/mssql/bin/sqlservr & ./init-db.sh

View File

@ -0,0 +1,3 @@
sleep 30s
/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P DockerCon!!! -i init-db.sql

View File

@ -0,0 +1,20 @@
CREATE DATABASE BulletinBoard;
GO
USE BulletinBoard;
CREATE TABLE Events (
Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
Title NVARCHAR(50),
Detail NVARCHAR(200),
[Date] DATETIMEOFFSET,
CreatedAt DATETIMEOFFSET NOT NULL,
UpdatedAt DATETIMEOFFSET NOT NULL
);
INSERT INTO Events (Title, Detail, [Date], CreatedAt, UpdatedAt) VALUES
(N'Docker for Beginners', N'Introduction to Docker using Node.js', '2017-11-21', GETDATE(), GETDATE()),
(N'Advanced Orchestration', N'Deep dive into Docker Swarm', '2017-12-25', GETDATE(), GETDATE()),
(N'Docker on Windows', N'From 101 to production', '2018-01-01', GETDATE(), GETDATE());
SELECT * FROM BulletinBoard.dbo.Events;

View File

@ -0,0 +1,4 @@
FROM nginx:1.13.6
RUN mkdir -p /data/nginx/cache
COPY nginx.conf /etc/nginx/nginx.conf

View File

@ -0,0 +1,43 @@
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=STATIC:30m inactive=24h max_size=200m use_temp_path=off;
proxy_pass_request_headers on;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header Connection keep-alive;
gzip on;
gzip_proxied any;
add_header X-Host $hostname;
add_header X-Cache-Status $upstream_cache_status;
map $sent_http_content_type $expires {
default off;
~image/ 6M;
}
server {
listen 80 default_server;
server_name _;
expires $expires;
location / {
proxy_pass http://bb-app:8080/;
proxy_cache STATIC;
proxy_cache_valid 200 1h;
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
}
}
}

29
docker-compose.yml Normal file
View File

@ -0,0 +1,29 @@
version: '3.3'
services:
bb-app:
image: sixeyed/bulletin-board-app
build:
context: ./bulletin-board-app
networks:
- bb-net
bb-db:
image: sixeyed/bulletin-board-db
build:
context: ./bulletin-board-db
networks:
- bb-net
bb-proxy:
image: sixeyed/bulletin-board-proxy
build:
context: ./bulletin-board-proxy
ports:
- "80:80"
networks:
- bb-net
networks:
bb-net: