╔══════════════════════╗ ╔═══════════╗
║ App.component.html.ts║-------➡║ index.html║-------
╚══════════════════════╝ ╚═══════════╝ |
↗ ↖ |
╔════════╗ ╔════════╗ |
║ Navbar ║ ║ Router ║ |
╚════════╝ ╚════════╝ |
↗ ↖ |
╔══════════════════════╗ ╔═══════════════════════╗ |
║ ProblemListCompinent ║ ║ ProblemDetailComponent║ |
╚══════════════════════╝ ╚═══════════════════════╝ |
↗ ↘ ↓ onInit function |
╔══════════════════════╗ ╔════════════╗ |
║ NewProblemCompinent ║ ➡ ║ DataService║ |
╚══════════════════════╝ ╚════════════╝ |
(api Request) ↑ ↓ ╔═══════════╗
-----------------------------↑ ↓ -------- ║public/ ║
↑ ↘ ║ index.html║
↑ ↘ ╚═══════════╝
╔══════════════╗ ╔═══════════╗ ↘ ↗ Index
║ProblemService║ ↔↔↔ ║Rest Router║ ↓ ↑ Router
╚══════════════╝ ║ rest.js ║ ╔═══════════╗
↓↑ ╚═══════════╝ ↖ ║ Server.js ║
↓↑ ╚═══════════╝
↓↑ ║
╔══════════════╗ ╔══════════╗ connect ║
║ ProblemModel ║ ←←← ║ MongoDB ║ ════════╝
╚══════════════╝ ╚══════════╝
- DataService(FrontEnd) call a Http Request
- for example getProblems()
- api Request send to server.js
- Server.js send request to RestRouter to find a right ProblemService
- Problem help to get data from ProblemSchema
- ProblemModel search the data from Database and send back to RestRouter
- RestRouter get the problems data and chagne it to a JSON file
- Send it back to DataService
- Since problem-list have subscripted, the chagne will call to frontend angular cli
- By data binding with html, frontend contents change
For testing
Made by npm, then, npm will install libraries by package.json file
Marked documents don't need to be uploaed eg. dependencies/node_module
- apps -> root -> src : the startpoint of our app
- outDir -> "dist" : files need to be uploaded (we'll change in the future)
- style -> style.css : global stylesheets (eg. bootstrap)
Composing HTML templates with Angularized markup
Classes to manage or support the templates.
Interacting with the VIEW through an API of properites and methods.
- selector : How to show the page in index.html
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
Adding application logic. Almost anything can be a service. (Logic Service / Data Service etc.)
- declarations : Component
- imports : Module
- providers : Service
- bootstrap : Enter
Boxing components and services. (收納箱)
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
╔ Router: app.routes.ts
║ ↓
║ ↓ ╔ Problem-list
╟ Conponents ══╟ Problem-detail
║ ╟ New Problem
║ ╚ Navbar
║ ↓ (without Navbar)
╚ Data Service ← ↙
- Made a dir. components in src/app/
- Used ag-cli automatically create four files
- and add ProblemListComponent in app.module.ts
$mkdir components
$ng g c problem-list
- install bootstrap & jQuery package from npm
- Since we don't use all bootstrap module, suggest to use from npm
npm install bootstrap --save
npm install jquery --save
- Add both jQuery nd Bootstrap in the script and stylesheet from ".angular-cli.json"
"styles": [
"styles.css",
"../node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": [
"../node_modules/jquery/dist/jquery.js",
"../node_modules/bootstrap/dist/js/bootstrap.js"
],
Use tag <app-problem-list> made by Angular in "app.component.html.ts"
could show the content from "problem-list.component.html"
index.html(<app-root>) <-
app.component.html.ts(<app-problem-list>) <-
problem-list.component.html.ts
selector: 'app-problem-list',
templateUrl: './problem-list.component.html',
- Opne a model file under the app
$ mkdir models
$ touch models/problem.model.ts
- Export a problem model
export class Problem{
id: number;
name: string;
desc: string;
diff: string;
}
- In problem-list component, we need to import the model and create a variable with mock datas
const PROBLEMS: Problem[] = [
{
id: 1,
name: "Two Sum",
desc: `Given an array of integers...`,
diff: "easy"
},
/// ...
];
-
In model, we will provide problems to VIEW which only can access content inside ProblemListComponent.
-
ngOnInit() : initization
-
getProblems(): give model PROBLEMS outside to the problems inside Component
-
:void : give the "type" of callback value to the method.
export class ProblemListComponent implements OnInit {
problems = []; // give a list
constructor() { }
ngOnInit() {
this.getProblems();
}
getProblems(): void{
this.problems = PROBLEMS;
}
}
- Build up a Structure with bootstrap
- "*ngFor": looping data from database and print it out
- Let each value in problems array save in a variable problem
*ngFor="let problem of problems"
- Gain the data from problems model and show on HTML markup (data binding)
{{problem.diff}}
- Also need a binding for class in span element, since we need to add-in a styling tag based on difficulty.
<span class="{{'pull-left label difficulty diff-' + problem.diff.toLowerCase()}}">
- problem-list.component.css
.difficulty {
min-width: 65px;
margin-right: 10px;
}
....
- Create a single reusable data service and inject it into the components that need it.
- to mock-problems.ts and export it
import { Problem } from "./models/problem.model";
export const PROBLEMS: Problem[] = [
//....
]
$cd src/app
$mkdir services
$cd services
$ng g s data
@NgModule({
providers: [
DataService
],
})
- Import problem model and mock problems
import { Injectable } from '@angular/core';
import { Problem } from '../models/problem.model';
import { PROBLEMS } from '../mock-problems';
- Add Method to help get singal problem with id and whole problems
getProblems():Problem[]{
return PROBLEMS;
}
getProblem(id: number): Problem{
return PROBLEMS.find((problem) => problem.id === id );
}
import {DataService} from '../../services/data.service';
problems: Problem[];
constructor(private dataService: DataService) { }
getProblems(): void{
this.problems = this.dataService.getProblems();
ng g c problem-detail
- Client side routing is the same as server side routing,
- but it's ran in the browser
touch app/app.routes.ts
- Import angular/router && Components
import { Routes, RouterModule } from '@angular/router';
import { ProblemListComponent } from './components/problem-list/problem-list.component';
import { ProblemDetailComponent } from './components/problem-detail/problem-detail.component';
const router: Routes =[
{
path: "",
redirectTo: 'problems',
pathMatch: 'full'
},
{
path: 'problems',
component: ProblemListComponent
},
{
path: 'problems/:id',
component: ProblemDetailComponent
},
{
path: '**',
redirectTo: 'problems'
}
];
export const routing = RouterModule.forRoot(router);
imports: [
BrowserModule,
routing
],
<router-outlet></router-outlet>
<a class="list-group-item" *ngFor="let problem of problems"
[routerLink]=['/problems',problem.id]>
- Add summary.pipe.ts
$ng -g -p summary
- Import Pipe, PipeTransform and Give a logic to SUMMARY
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: "summary"
})
export class SummaryPipe implements PipeTransform {
transform(value: string, limit?: number){
if (!value)
return null;
return value.substr(0,70) + '...';
}
}
- Add SummaryPipe into Module
@NgModule({
declarations: [
AppComponent,
ProblemListComponent,
ProblemDetailComponent,
SummaryPipe
],
- Used Summary in html markup
<div class="list-group-item description "> {{problem.desc | summary}}</div>
- Import Probelm from model
import { Problem } from '../../models/problem.model';
export class ProblemDetailComponent implements OnInit {
problem : Problem[];
}
- Import Data from DataService and Gain data from ActivedRoute
import { DataService } from '../../services/data.service';
import { ActivatedRoute, Params} from '@angular/router';
constructor(private dataService: DataService, private route: ActivatedRoute) { }
- Used ngOnInit to get the id from Route and change the parameter from String into Number
ngOnInit() {
this.route.params.subscribe(params => {
this.problem = this.dataService.getProblem(+params['id']); // Change String into Number
})
}
- Add HTML markup (*ngIf to show existed problem)
<div class="container" *ngIf = "problem">
- Add new-problem component
$ ng g c new-problem
- Import Form module
imports: [
BrowserModule,
routing,
FormsModule
],
- HTML markup for New Problem Form
- [()]: "Banana in the Box" for TWO-WAY data binding
- []: Property Binding (One-Way)
- (): Event Binding (One-Way)
- Input Name +
<div>
<form #formRef = "ngForm">
<div class="form-group">
<label for="problemName">Problem Name</label>
<input name="problemName" id="problemName"
class="form-control" type="text" required
placeholder="Please Enter Problem Name"
[(ngModel)]="newProblem.name">
</div>
<div></div>
.
.
.
</form>
</div>
- Select Difficulities <\select> + <\option>
<div class="form-group">
<label for="problemDiff">Difficulty</label>
<select name="diff" id="diff" class="form-control"
[(ngModel)]="newProblem.diff">
<option *ngFor = "let diff of diffs" [value] = "diff">
{{diff}}
</option>
</select>
</div>
- Type Area <\textarea><\textarea>
<div class="form-group">
<label for="problemDesc">Problem Description</label>
<textarea name="problemDesc" id="problemDesc"
class="form-control" required
placeholder="Please Enter Problem Description"
[(ngModel)]="newProblem.desc" rows="3">
</textarea>
</div>
- Submit Button (with Click EVENT)
<div class="row">
<div class="col-md-12">
<button type="submit" class="btn btn-info pull-roght"
(click) = "addProblem()">
Add Problem
</button>
</div>
</div>
- Import Problem Model into new-problem.component and Add a string array of difficulities
import { Problem } from '../../models/problem.model';
- Give a const variable of default Problem
const DEFAULT_PROBLEM: Problem = Object.freeze({
id:0,
name:'',
desc:'',
diff:'easy'
});
- Assign Value to Prpblem and then send the data to DataService for processing (by addProblenm())
- Import DataService
- Connent to Service in constructor
- Chagne new Problem into Default Problem after each added
newProblem: Problem = Object.assign({}, DEFAULT_PROBLEM);
diffs: string[] = ['easy', 'medium', 'hard', 'super'];
constructor(private dataService: DataService) { }
addProblem(){
this.dataService.addProblem(this.newProblem);
this.newProblem = Object.assign({}, DEFAULT_PROBLEM);
}
- dataService modify: Since we couldn't change the array of PROBLEMS, we saved it to another variable for adding new Problems
problems: Problem[] = PROBLEMS;
- Also, change the original PROBLEM to this.problems which we setted last step
getProblems():Problem[]{
return this.problems;
}
- Add a method of "addProblem()"
- Give a new problem an id and push this object into problems array
addProblem(problem: Problem): void{
problem.id = this.problems.length + 1;
this.problems.push(problem);
}
- create files for navbar
ng g c navbar
- Put navbar in app.component.html
<app-navbar></app-navbar>
<router-outlet></router-outlet>
- Copy navbar codes from bootstrap
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-2" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
......
- install font-awsome
npm install --save font-awesome
- Add font-awesome stylesheet in ".angular-cli.json"
"../node_modules/font-awesome/css/font-awesome.css"
- Add html
<div class="container">
<div class="social-icons">
<ul class="list-inline">
<li><a href="mailto:[email protected]?Subject=Visiter from your website" target="_blank" ><i class="fa fa-envelope"></i></a></li>
<li><a href="https://github.com/WeiChienHsu" target="_blank"><i class="fa fa-github" ></i></a></li>
<li><a href="https://www.linkedin.com/in/weichien-hsu/" target="_blank"><i class="fa fa-linkedin"></i></a></li>
</ul>
</div> <!-- /.social-icons -->
</div>
cp -R week1 week2
mkdir oj-server
- Add scripts > "start": "node server.js"
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
- initize npm
npm init
- Add .gitignore
touch .gitignore
# dependencies
/node_modules
- install express
npm install express --save
- User can GET and POST problems from server using RESTful API
- Handle server-side routing
- Create server.js
- Start project with nodemon
- GET /api/v1/problems (get all problems)
- GET /api/v1/problems/:id (get problem by id)
- POST /api/v1/problems (add a new problem)
- Add Router to server.js
- Create problemService to READ/WRITE the problem data (Mock data)
- Test with POSTMAN
- Handle GET /api/v1/problems/:id
- Handle POST /api/vi/problems requests
- npm install body-parser --save
- Test with POSTMAN
- open server.js file and C&P init document
const express = require('express')
const app = express()
app.get('/', (req, res) => res.send('Hello World!'))
app.listen(3000, () => console.log('Example app listening on port 3000!'))
- give a ROUTER to deal with specific stuff then we can clearify what the requests are asked for easier
app.use('/api/v1', restRouter);
- Open a folder "routes" to deal with all routing problem
- add new file "rest.js" (writting in express)
mkdir routes
touch routes/rest.js
const express = require('express');
const router = express.Router();
const problemService = require('../services/problemService');
- Don't need to write /api/vi since we have alrady set up by useing "app.use" in server.js
- Add a promise
- For example, get a request of "getProblem()" and send a problems out
router.get('/problems', (req, res) => {
problemService.getProblems()
.then(problems => res.json(problems));
});
- Export router
module.exports = router;
- in server.js, import restRouter
const restRouter = require('./routes/rest.js');
- To deal with data, we need a problemService to connect with our router
mkdir services
touch services/problemService.js
- Add a mock data since we haven't touch database
problems = [
....
]
- Give a function to get Problems and export it.
const getProblems = function(){
console.log('In the problem service get problems')
return new Promise((resolve, reject) => {
resolve(problems);
})
}
const getProblem = function(){}
module.exports = {
getProblems,
getProblem
}
- In rest.js, given a request parameter(id) to getProblem() in problemService to catch the problem we needed
- give a + to change id from string into number
router.get('/problems/:id', (req, res) =>{
const id = req.params.id;
problemService.getProblem(+id)
.then(problem => res.json(problem))
} )
- In problemService.js
const getProblem = function(id){
console.log("In the problem service get single problem");
return new Promise((resolve, reject) => {
resolve(problems.find(problem => problem.id === id));
});
}
- Body Parser (jsonparser) help to take JSON object from body for sending POST request.
- In rest.js
npm install body-parser --save
const bodyParser = require('body-parser');
const jsonParser = bodyParser.json();
- Post Problem
- Need a jsonParser as a middleware to transfer "req.body" into JSON object
router.post('/problems',jsonParser, (req, res) => {
problemService.addProblem(req.body)
.then(problem => { //resolve
res.json(problem);
},(error) => { //reject
res.status(400).send('Problem name already exists!');
})
})
- Add addProblem function in problemService
const addProblem = function(newProblem){
return new Promise((resolve, reject) => {
if (problems.find(problem => problem.name === newProblem.name)){
reject('Problem already exists!');
} else {
newProblem.id = problems.length + 1;
problems.push(newProblem);
resolve(newProblem);
}
});
}
- GET problem by typing in address and it'll send out out mock data in JSON
- Reject : frontend will have a handler such as the same JS promise
"http://localhost:3000/api/v1/problems"
- POST problem will be send as a JOSN file in body and then we need to use body-parser to read the content in backend
- Send a JOSN object POST request by Postman
- Same name sent, should respone "Problem name already exists!"
{
"name": "3Sum",
"desc": "Given an array S of n integers, are there elements a, b, c in S such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero.",
"difficulty": "medium"
}
- Post a right request will respone right messages with new id
{
"name": "31Sum",
"desc": "Given an array S of n integers, are there elements a, b, c in S such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero.",
"difficulty": "medium",
"id": 6
}
- Register an account on mLab and create a database
- Install mongoose and connect to MongoDB on mLab
npm install mongoose --save
- Add schema problemModel
- refactor problemService to READ/WRITE data FROM/TO MongoDB
- Reqire and connect mongoose
- server.js
const mongoose = require('mongoose');
mongoose.connect('mongodb://user:[email protected]:23976/cs503-1705test');
mkdir models
touch models/problemModel.js
- problemModel.js
const mongoose = require('mongoose');
const ProblemSchema = mongoose.Schema({
id: Number,
name: String,
desc: String,
diff: String
});
const ProblemModel = mongoose.model('ProblemModel', ProblemSchema);
module.exports = ProblemModel;
-
In problemService, we need to get problem from database
-
No longer need the old getProblem function since it gets problem from our mock data.
-
Directly get problems from database
-
ProblemModel.find(condition, callback[err, data]) : no condition and callback first deal with error and reject or resolve(send back) the data
- findOne({id: neameYouInput}, (err, problem))
const getProblem = function(id){
return new Promise((resolve, reject) => {
ProblemModel.findOne({id: id}, (err, problem) => {
if (err) {
console.log("In the problem service get problem");
reject(err);
} else {
resolve(problem);
}
});
});
}
const getProblem = function(id){
return new Promise((resolve, reject) => {
ProblemModel.findOne({id: id}, (err, problem) => {
if (err) {
console.log("In the problem service get problem");
reject(err);
} else {
resolve(problem);
}
});
});
}
- If found a same id which means the data exist, we need to reject
- Count the problems and assign a new id
- Create a mongoProblem for sending data to MongoDB (use mongoProblem.save())
const addProblem = function(newProblem){
return new Promise((resolve, reject) => {
ProblemModel.findOne({name: newProblem.name}, (err, data) => {
if (data) {
// find a same id
reject('Problem already exists!');
} else {
ProblemModel.count({}, (err, count) => {
newProblem.id = count + 1;
//Save into MongoDB
const mongoProblem = new ProblemModel(newProblem);
mongoProblem.save();
resolve(mongoProblem);
});
}
});
});
}
- Refactor client-side data.service to async
- in app.module.ts
- import HttpClientModule
- Refactor all components calling data.service
- Problem-list.component.ts
- Problem-detail.component.ts
- New-Problem.component.ts
- Update .angualr-cli.json, change location of ourDir
- In the future, you can use ng build -- watch in/ oj-client, we are not using localhost:4200 anymore
- Send static web pages from server to browser
- Solve "refresh" issue
- Call out DataService which used to connect with mock data
- Http Module (in app.module)
import { HttpClientModule } from '@angular/common/http';
imports: [
BrowserModule,
routing,
FormsModule,
HttpClientModule
],
-
Import HttpClient, HttpHeaders, HttpResponse:
-
Observable: Observe Data Flow. Non-stop sending data, with Values, Complete, Error.
-
BehaviorSubject: Always exist.
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Rx';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/toPromise';
- No longer need the mock problem and change it to the problemSource putting all problems inside and marks it as private
// problems: Problem[] = PROBLEMS;
private _problemSource = new BehaviorSubject<Problem[]>([]);
- Register Angular HttpClient
constructor(private HttpClient: HttpClient) { }
- Call Api v1, Endpoint ('api/v1/problem')
- Transfer BehaviorSubject into Promise
- "Next" : receive the changes and check the latest data from database
- "Catch" : for frontend to handle Error, if there's error, use handleError method
getProblems():Observable<Problem[]>{
this.httpClient.get('api/v1/problems')
.toPromise()
.then((res: any) => {
this._problemSource.next(res);
})
.catch(this.handleError);
return this._problemSource.asObservable();
}
- Create a function to handle Error
private handleError(error: any): Promise<any> {
return Promise.reject(error.body || error);
}
getProblem(id: number): Promise<Problem>{
return this.httpClient.get(`api/v1/problems/${id}`)
.toPromise()
.then((res: any) => res)
.catch(this.handleError);
}
-
Since we need to send a POST request to API, we need to give a hearder first (Content-Type).
-
Post Request will send url+body+header and a callback function
-
Call getProblems: since frontend won't know the change of database when we add a problem so after we update problem list, we need to call back new problem list in frontend
addProblem(problem: Problem) {
const options = { headers: new HttpHeaders({'Content-Type': 'application/json' })};
return this.httpClient.post('api/v1/problems', problem, options)
.toPromise()
.then((res: any) => {
this.getProblems();
return res;
})
.catch(this.handleError);
}
- Import OnDestroy/Subscription
- subcribe when OiInit, then when the problem data change, our frontend will know
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
- subscription
subscriptionProblems: Subscription;
- Oninit
- OnDestroy
ngOnInit() {
this.getProblems();
}
ngOnDestroy(){
this.subscriptionProblems.unsubscribe();
}
- change the getProblem from sync to async
getProblems(): void{
this.subscriptionProblems = this.dataService.getProblems()
.subscribe(problems => this.problems = problems);
}
- getProblem callback is a Promise, we need to change how onInit works.
ngOnInit() {
this.route.params.subscribe(params => {
this.dataService.getProblem(+params['id'])
.then(problem => this.problem = problem)
});
- Dont need to change, we didn't use the data from database
- Set all UI documents will into publuc
- In .angular-cli.json, change "outDir" to '../public'
- in Oj-client, and heres a public file
ng build
- In server.js, add another router to deal with pade localhost3000
app.use('/', indexRouter);
- In index.js, use "Path" from nodeJS to get the content from public when someone send a request asking localhost3000
const express = require('express');
const router = expree.Router();
const path = require('path');
router.get('/', (req, res) => {
res.sendFile('index.html', {root: path.join(__dirname, '../../public/')});
});
module.exports = router;
- Fail to load the static files such as JS, CSS, HTML
localhost/:13 GET http://localhost:3000/inline.bundle.js net::ERR_ABORTED
localhost/:13 GET http://localhost:3000/polyfills.bundle.js net::ERR_ABORTED
localhost/:13 GET http://localhost:3000/scripts.bundle.js net::ERR_ABORTED
localhost/:13 GET http://localhost:3000/styles.bundle.js net::ERR_ABORTED
localhost/:13 GET http://localhost:3000/vendor.bundle.js net::ERR_ABORTED
localhost/:13 GET http://localhost:3000/main.bundle.js net::ERR_ABORTED
- Give a path
const path = require('path');
app.use(express.static(path.join(__dirname, '../public/')));
- Server couldn't deal with frontend router
- After finishing indexRouter and restRouter logic, backend will directly send back the index.html nomether what request fronend (client) send
app.use((req, res) => {
res.sendFile('index.html', {root: path.join(__dirname, '../public/')})
})
- Create Editor Component
- Embedding Ace Editor (third party)
- Add language select, reset and submit buttons
- Install socket.io on oj-client and oj-server
- Establish socket connection
- Synchronize the editor buffer content
- Store and restore socket sessions with Redis
-Copy the week2 code
cp -r week2 week3
- Create Editor Component
ng g c editor
- In porblem details html
- Hidden in x samll screen
<div class="hidden-xs col-md-8">
<app-editor></app-editor>
</div>
- Install package
npm install --save ace-builds
- Add JS packages in .angular-cli
- src-min-noconflict for not conflicting
"scripts": [
"../node_modules/jquery/dist/jquery.js",
"../node_modules/bootstrap/dist/js/bootstrap.js",
"../node_modules/ace-builds/src-min-noconflict/ace.js",
"../node_modules/ace-builds/src-min-noconflict/mode-java.js",
"../node_modules/ace-builds/src-min-noconflict/mode-python.js"
],
- Declare ace in component.ts
declare const ace: any;
- Add Script in "ngOnInit()"
- set a editor value
- Add an editor variable inside class
export class EditorComponent implements OnInit {
editor: any;
ngOnInit() {
this.editor = ace.edit("editor");
this.editor.setTheme("ace/theme/eclipse");
this.editor.getSession().setMode("ace/mode/java");
this.editor.setValue(this.defaultContent['Java'])
}
- Add Default Content
defaultContent = {
'Java': `public class Example {
public static void main(String[] args) {
// Type your Java code here
}
}`,
'Python': `class Solution:
def example():
# Write your Python code here`
};
- HTML
<div id="editor"></div>
- CSS
- media: screen
@media screen {
#editor {
height: 600px;
}
}
- Styling
#editor {
height: 600px;
}
.lang-select {
width: 100px;
margin-right: 10px;
}
header .btn {
margin: 0 5px;
}
footer .btn {
margin: 0 5px;
}
.editor-footer, .editor-header {
margin: 10px 0;
}
.cursor {
/*position:absolute;*/
background: rgba(0, 250, 0, 0.5);
z-index: 40;
width: 2px !important;
}
- Choose language
- setLanguage()
<section>
<header class="editor-header">
<select class="form-control pull-left lang-select" name="language"
[(ngModel)]="language" (change)="setLanguage(language)">
<option *ngFor="let language of languages" [value]="language">
{{language}}
</option>
</select>
- Reset Button
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#myModal">
Reset
</button>
- Modal Dalogue
- With reset editor()
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Are you sure</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
You will lose current code in the editor, are you sure?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" data-dismiss="modal"
(click)="resetEditor()">Reset</button>
</div>
</div>
</div>
</div>
</header>
- Editor inside Row
<div class="row">
<div id="editor"></div>
</div>
- Submit button
<footer class="editor-footer">
<button type="button" class="btn btn-success pull-right"
(click)="submit()">Submit Solution</button>
</footer>
</section>
- Setup languages and default language
languages: string[] = ['Java', 'Python'];
language: string = 'Java';
- resetEditor
resetEditor(): void {
this.editor.setValue(this.defaultContent[this.language]);
}
- setLanguage
setLanguage(language: string): void{
this.language = language;
this.resetEditor();
}
- subimt
submit(): void{
const userCode = this.editor.getValue();
console.log(userCode); // temp
}
- call resetEditor when Oninit
- move the getSession function to reset function and change its theme based on language we chose
ngOnInit() {
this.editor = ace.edit("editor");
this.editor.setTheme("ace/theme/eclipse");
this.resetEditor();
this.editor.$blockScrolling = Infinity;
}
resetEditor(): void {
this.editor.setValue(this.defaultContent[this.language]);
this.editor.getSession().setMode("ace/mode/" + this.language.toLocaleLowerCase());
}
- Install socket.io
npm install --save socket.io
- Add module in client side and add in app.module
ng g s collaboration
- App.module
providers: [
DataService,
CollaborationService
],
- .angular-cli
"../node_modules/socket.io-client/dist/socket.io.js"
- collaboration.service
- Every times when you connect, socket send a message to window.location.origin (Backend server Endpoint)
init(): void {
this.collaborationSocket = io(window.location.origin, {query: "message=" + "haha"});
- Add Event handler to receive message
this.collaborationSocket.on('message', (message) => {
console.log('message received from server' + message);
});
- Import Collaboration Service in editor component, when editor init, it will call an collaboration.init() function
constructor( private collaboration: CollaborationService) { }
ngOnInit(){
this.collaboration.init();
}
- Install socket.io
npm install --save socket.io
- Open a file for editor Socket Service
touch editorSocketService.js
- Receive message from client
module.exports = function(io){
io.on('connection', (socket) => {
console.log(socket);
const message = socket.handshake.query['message'];
console.log(message);
- Send back message to client
io.to(socket.id).emit('message', 'hehe from server');
- Build up a Http server in server.js
const http = require('http');
const socketIO = require('socket.io');
const io = socketIO();
const editorSocketService = require('./services/editorSocketService')(io);
- Open a new line for server
const server = http.createServer(app);
io.attach(server);
server.listen(3000);
server.on('listening', onListening);
function onListening(){
console.log('App listening on port 3000')
}
-
Edit Component get a "session id" from activeRoute (the same way we get that id from problem list to problem detail)
-
Send id to collaboration service to tell where Url the user now located
-
Collaboratoin will send the same information to editorSocketService
- Import
import { ActivatedRoute, Params } from '@angular/router';
export class EditorComponent implements OnInit {
sessionId: string;
constructor( private collaboration: CollaborationService,
private route: ActivatedRoute) { }
-
In ngOnInit, get id first by route params senging into "sessionId" and call initEditor (Make those methods a function)
-
Send editor and session id when using clollaboration init
-
collaboration.init(this.editor, this.sessionId)
-
add lastAppliedChange in editor to set up collaboration socket
-
When there's change happened ,register change callback --> to collaboration.service
ngOnInit() {
this.route.params
.subscribe(params => {
this.sessionId = params['id'];
this.initEditor();
});
}
initEditor(){
this.editor = ace.edit("editor");
this.editor.setTheme("ace/theme/eclipse");
this.resetEditor();
this.editor.$blockScrolling = Infinity;
// set up collaboration secket
this.collaboration.init(this.editor, this.sessionId);
this.editor.lastAppliedChange = null;
// register changne callback
this.editor.on('change', (e) => {
console.log('editor change' + JSON.stringify(e));
if (this.editor.lastAppliedChange != e) {
this.collaboration.change(JSON.stringify(e));
}
})
}
-
Send in both editor and sessionId to service
-
service listen to editor component, when there's a chagne message sent in, show the change in editor(in editor component) and send the chagne delta to Server (in collaboration service)
init(editor: any, sessionId: string): void {
this.collaborationSocket = io(window.location.origin, {query: "sessionId=" + sessionId});
this.collaborationSocket.on('change', (delta: string) => {
delta = JSON.parse(delta);
editor.lastAppliedChange = delta;
editor.getSession().getDocument().applyDeltas([delta]);
});
}
- Also, send the change to Server by sockets
change(delta: string): void {
this.collaborationSocket.emit('change', delta);
}
- delta
editor change{"start":{"row":6,"column":4},"end":{"row":6,"column":5},"action":"insert","lines":["a"]}
-
collaborations to record who is in this problem
-
receive message from collaboration "{query: "sessionId=" + sessionId}", and save into sessionId
-
save sessionId into socket.id (user)
-
if sessionId is the first one which means not in collaboration, creates a new collaboration oject with participants
-
If the sessionId is in collaboration, add sockt.id into participants
module.exports = function(io){
//collaboration sessions
const collaborations = {};
// map form socketId to sessionId
const socketIdToSessionId = {};
io.on('connection', (socket) => {
const sessionId = socket.handshake.query['sessionId'];
socketIdToSessionId[socket.id] = sessionId;
if (!sessionId in collaborations) {
collaborations[sessionId] = {
'participants':[]
};
}
collaborations[sessionId]['participants'].push(socket.id);
});
}
- If Service revceive change, save the sessionId and array of participants. Then, send the changes to all participants.
socket.on('change', delta => {
const sessionId = socketIdToSessionId[socket.id];
if (sessionId in collaborations){
const participants = collaborations[sessionId]['participants'];
for (let participant of participants) {
if (socket.id !== participant){
io.to(participant).emit('change', delta)
}
}
} else {
console.error('error')
}
});
- collaboration service, when user
- 從connection的時候就先去collaborations看有沒有這個sessionId
- 如果 collaborations 裡面沒有, 要從Redis裡面拿(restoreBuffer)
- 拿到 sessionId,到collections裡面看有沒有,如果沒有,直接說“沒有”
- 如果有,直接到collections內拿記錄下來的"cachedInstructions:
- 然後把 instrcutions裡面從頭到尾把所有變化讀取, [0] "change" [1] delta
restoreBuffer():void{
this.collaborationSocket.emit('restoreBuffer');
}
- editor component
this.collaboration.restoreBuffer();
- In Linus
wget http://download.redis.io/releases/redis-3.2.6.tar.gz
tar xzf redis-3.2.6.tar.gz
cd redis-3.2.6
make
sudo make install
cd utils
sudo ./install_server.sh`
- oj-server
npm install --save redis
- build up a dir modules -> redisClient.js
mkdir modules
touch modules/redisClient.js
const redis = require('redis');
const client = redis.createClient();
function set(key, value, callback) {
client.set(key, value, function(err, res) {
if (err) {
console.log(err);
return;
}
callback(res);
});
}
function get(key, callback) {
client.get(key, function(err, res) {
if (err) {
console.log(err);
return;
}
callback(res);
});
}
function expire(key, timeInSeconds) {
client.expire(key, timeInSeconds);
}
function quit() {
client.quit();
}
module.exports = {
get,
set,
expire,
quit,
redisPrint: redis.print
}
const redisClient = require('../modules/redisClient');
const TIMEOUT_IN_SECONDS = 3600;
- module.exports
const sessionPath = '/temp_sessions/';
- Find if user is the first one get into this problem by looking for collaborations (if there's another user) or for redis (in the timeset)
if (sessionId in collaborations) {
collaborations[sessionId]['participants'].push(socket.id);
} else {
redisClient.get(sessionPath + '/' + sessionId, data => {
if (data) { // there is data in radis
console.log('session terminated perviously, pulling back from redis');
collaborations[sessionId] = {
'cachaedInstructions' : JSON.parse(data),
'participants': []
}
} else { // create a new collaboration
console.log('creating new session');
collaborations[sessionId] = {
'cachaedInstructions' : [],
'participants': []
}
}
collaborations[sessionId]['participants'].push(socket.id);
});
}
- Add a cachaedInstructions if there is a sessionId in collaborations when there is a change message
socket.on('change', delta => {
const sessionId = socketIdToSessionId[socket.id];
if(sessionId in collaborations){
collaborations[sessionId]['cachaedInstructions'].push(['change', delta, Date.now()]);
}
- When Client side call restore Buffer, emit out all contents saved in redis
- instruction[0] : change
- instruction[1] : content
- instruction 就是紀錄每一行的動作
socket.on('restoreBuffer', () => {
const sessionId = socketIdToSessionId[socket.id];
if (sessionId in collaborations) {
const instructions = collaborations[sessionId]['cachaedInstructions'];
for (let instruction of instructions) {
socket.emit(instruction[0], instruction[1]);
}
}
});
- When disconnect, save content in radis
- Remove user's sessionId from participants
- See if the last user lefts (participants.length === 0)
socket.on('disconnrect', () => {
const sessionId = socketIdToSessionId[socket.id];
let foundAndRemove = false;
if (sessionId in collaborations) {
const participants = collaborations[sessionId]['participants'];
const index = participants.indexOf(socket.id);
if (index >= 0){
participants.slice(index, 1);
foundAndRemove = true;
if (participants.length === 0){ //last user
const key = sessionPath + '/' + sessionId;
const value = JSON.stringify(collaborations[sessionId]['cachaedInstructions']);
redisClient.set(key, value, redisClient.redisPrint);
redisClient.expire(key, TIMEOUT_IN_SECONDS);
delete collaboraitons[sessionId];
}
}
}
if (!foundAndRemove) {
console.error('warning');
}
});
- Show Result on UI
- Add build_and_run API call in Node.js Server
- Build a web server with Flask
- Install Docker
- Create Dockerfile
- Docker build/push/pull
- Build Executor
-Copy the week2 code
cp -r week3 week4
Step1 : In editor, we need to display compile and execute output. Therefore, we need to add a div right after the ACE editor in editor component.
- In Editor Component htnl and ts
<div>
{{output}}
</div>
'Python': `class Solution:
def example():
# Write your Python code here`
};
output: string = '';
Step2 : Build and Run are performed on server side. On the client side, we just make a service call and display the results. Here we call DataService to make the call.
Step3: Add buildAndRun in DataService. Basically, it is just a post request to Node.js server then getting results. The target of post request is /api/vi/results
-
NoteL client doesn't know about executor, to client, it's only talking to the Node.js server
-
Send usercode and language datas to Server side (by DataService)
buildAndRun(data): Promise<any> {
const options = { headers: new HttpHeaders({ 'Content-Type': 'application/json'})};
return this.httpClient.post('api/v1/build_and_run', data, options)
.toPromise()
.then(res => {
console.log(res);
return res;
})
.catch(this.handleError);
}
- Import DataService in editor component and send out the userCodes and language data
- Also add in constructor
- data as a JSON object sent to server side
submit(): void {
const userCodes = this.editor.getValue();
const data = {
userCodes: userCodes,
lang: this.language.toLocaleLowerCase()
};
this.dataService.buildAndRun(data)
.then(res => this.output = res.text);
}
private dataService: DataService
- Handle the Router in rest.js
router.post('/build_and_run', jsonParser, (req, res) => {
const userCodes = req.body.userCodes;
const lang = req.body.lang;
console.log('lang: ', lang, 'usercode: ', userCodes);
res.json({'text': 'hello from nodejs'});
});
npm install --save node-rest-client
- In rest.js import restClient and register
const nodeRestClient = require('node-rest-client').Client;
const restClient = new nodeRestClient();
EXECUTOR_SERVER_URL = 'http://localhost:5000/build_and_run';
restClient.registerMethod('build_and_run', EXECUTOR_SERVER_URL, 'POST');
- Modify the build_and_run rout
router.post('/build_and_run', jsonParser, (req, res) => {
const userCodes = req.body.userCodes;
const lang = req.body.lang;
console.log('lang: ', lang, 'usercode: ', userCodes);
restClient.methods.build_and_run(
{
data: {code: userCodes, lang: lang},
headers: { 'Content-Type': 'application/json'}
},
(data, response) => {
// build: xxx ; run: xxx
const text = `Build output: ${data['build']}. Execute Output: ${data['run']}`;
data['text'] = text;
res.json(data);
}
);
});
- Build up a Execute Server
mkdir executor
cd executor
sudo apt-get update
sudo apt-get -y install python3-pip
sudo pip3 install Flask
touch requiremnet.txt
Flast
- When you went to anoter developeing enviroment, could automatically intsall
sudo pip3 install -r requirement
- Add a Flash server, test the connection in localhost:5000 (default)
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'hello world'
if __name__ == '__main__':
app.run(debug = True)
@app.route('/build_and_run', methods = ['POST'])
def build_and_run():
return
if __name__ == '__main__':
app.run(debug = True)
- Transfer the output to JSON
import json
from flask import Flask
app = Flask(__name__)
from flask import jsonify
from flask import request
- Receive data from Node Server
@app.route('/build_and_run', methods=['POST'])
def build_and_run():
data = request.get_json()
if 'code' not in data or 'lang' not in data:
return 'You should provide "code" and "lang"'
code = data['code']
lang = data['lang']
print("API got called with code: %s in %s" % (code, lang))
return jsonify({'build': 'build jajaja', 'run': 'run from oajsfoaij'})
- Install Docker
curl -fsSL https://get.docker.com/ | sh
Setup docker permission:
sudo usermod -aG docker $(whoami)
(you need to logout and login again after set permission)
To start docker when the system boots: sudo systemctl enable docker
- Dockerfile
FROM ubuntu:16.04
MAINTAINER Kevin Hsu
RUN apt-get update
RUN apt-get install -y openjdk-8-jdk
RUN apt-get install -y python3
- Build Docker
sudo docker build -t weichienhsu/coj_project
docker login
docker push weichienhsu/coj_project
- Docker Package
pip3 install docker
- executor_utils.py
import docker
import os
import shutil
import uuid
from docker.errors import APIError
from docker.errors import ContainerError
from docker.errors import ImageNotFound
- .gitignore
*.pyc
- For Docker: source file names / binary names/ build commands/ execute commands
SOURCE_FILE_NAMES = {
"java": "Example.java",
"python": "example.py"
}
BINARY_NAMES = {
"java": "Example",
"python": "example.py"
}
BUILD_COMMANDS = {
"java": "javac",
"python": "python3"
}
EXECUTE_COMMANDS = {
"java": "java",
"python": "python3"
}
- Create a file tmp to save current_dir
CURRENT_DIR = os.path.dirname(os.path.relpath(__file__))
IMAGE_NAME = 'weichienhsu/coj_project'
client = docker.from_env()
TEMP_BUILD_DIR = "%s/tmp/" % CURRENT_DIR
CONTAINER_NAME = "%s:latest" % IMAGE_NAME
- Load image: first from client, if not found, pull from docker
- for I/O we always need to try and expect
def load_image():
try:
client.images.get(IMAGE_NAME)
print("Image exists locally")
except ImageNotFound:
print('image not found locally. lodaing from docker')
client.images.pull(IMAGE_NAME)
except APIError:
print('docker hub go die')
return
print('image loaded')
- Make a directory
def make_dir(dir):
try:
os.mkdir(dir)
except OSError:
print('go die')
- Build and run function
def build_and_run(code, lang):
result = {'build': None, 'run': None, 'error': None}
source_file_parent_dir_name = uuid.uuid4()
source_file_host_dir = "%s/%s" % (TEMP_BUILD_DIR, source_file_parent_dir_name)
source_file_guest_dir = "/test/%s" % (source_file_parent_dir_name)
make_dir(source_file_host_dir)
with open("%s/%s" %(source_file_host_dir, SOURCE_FILE_NAMES[lang]), 'w') as source_file:
source_file.write(code)
try:
client.containers.run(
image=IMAGE_NAME,
command="%s %s" % (BUILD_COMMANDS[lang], SOURCE_FILE_NAMES[lang]),
volumes={source_file_host_dir: {'bind': source_file_guest_dir, 'mode': 'rw'}},
working_dir=source_file_guest_dir
)
print('source built')
result['build'] = 'OK'
except ContainerError as e:
result['build'] = str(e.stderr, 'utf-8')
shutil.rmtree(source_file_host_dir)
return result
try:
log = client.containers.run(
image=IMAGE_NAME,
command="%s %s" % (EXECUTE_COMMANDS[lang], BINARY_NAMES[lang]),
volumes={source_file_host_dir: {'bind': source_file_guest_dir, 'mode': 'rw'}},
working_dir=source_file_guest_dir
)
log = str(log, 'utf-8')
result['run'] = log
except ContainerError as e:
result['run'] = str(e.stderr, 'utf-8')
shutil.rmtree(source_file_host_dir)
return result
shutil.rmtree(source_file_host_dir)
return result
- For executor_server
- Import executor_utils
import executor_utils as eu
# return jsonify({'build': 'build jajaja', 'run': 'run from oajsfoaij'})
result = eu.build_and_run(code, lang)
return jsonify(result)
- init the loading from eu
if __name__ == '__main__':
eu.load_image()
app.run(debug=True)
- If Couldn't find docker module
pip install docker
-
If frontend didn't change anymore, you could build a production version.
-
There is no .map file and you couldn't debug on browser
-
In oj-client
ng build --prod
- Script to settle all server and executer missions
- launcher.sh
- Remove executing localhost at first
- Start redis
- run server & -> keep runing
- run executor &
- echo
- presskey to terminate processes
#! /bin/bash
fuser -k 3000/tcp
fuser -k 5000/tcp
service redis_6379 start
cd ./oj-server
nodemon server.js &
cd ../executor
pip3 install -r requirements.txt
python3 executor_server.py &
echo "=============================="
read -p "PRESS [enter] to terminate processes." PRESSKEY
fuser -k 3000/tcp
fuser -k 5000/tcp
service redis_6379 stop
- bash ./launcher.sh
deb http://nginx.org/packages/ubuntu/ venial nginx
deb-src http://nginx.org/packages/ubuntu/ xenial nginx
sudo apt-get update
sudo apt-get install nginx
sudo service nginx start
- oj-server:
nodemon server.js
- executor:
python3 executor_server.py
- Redis:
redis-server
- Docker
sudo ducker run weichienhsu/coj-project
- oj-client:
npm install
- oj-server:
npm install