From d688c60b49d160c3adb8c807e5fa58bbe33caff0 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Tue, 11 Oct 2016 13:42:17 -0400 Subject: [PATCH] React setup, ajax retrieval example, live reload, auto SCSS compile --- Dockerfile | 5 -- README.md | 5 +- app/api/projects.py | 1 + app/static/app/bundles/.gitignore | 2 +- app/static/app/css/main.css | 55 ---------------- app/static/app/css/main.css.map | 7 --- app/static/app/js/Dashboard.jsx | 13 ++++ app/static/app/js/app.jsx | 11 ---- app/static/app/js/components/ProjectList.jsx | 62 +++++++++++++++++++ .../app/js/components/ProjectListItem.jsx | 29 +++++++++ .../js/components/ProjectListItemPanel.jsx | 17 +++++ app/static/app/js/css/Dashboard.scss | 3 + app/static/app/js/index.jsx | 5 -- app/static/app/js/main.js | 38 ------------ app/static/app/js/main.jsx | 6 ++ app/templates/app/base.html | 44 ++++++++++++- app/templates/app/dashboard.html | 2 +- docker-compose.yml | 1 + package.json | 11 +++- start.sh | 3 + webpack-server.js | 15 +++++ webpack.config.js | 36 +++++++++-- 22 files changed, 237 insertions(+), 134 deletions(-) delete mode 100644 app/static/app/css/main.css delete mode 100644 app/static/app/css/main.css.map create mode 100644 app/static/app/js/Dashboard.jsx delete mode 100644 app/static/app/js/app.jsx create mode 100644 app/static/app/js/components/ProjectList.jsx create mode 100644 app/static/app/js/components/ProjectListItem.jsx create mode 100644 app/static/app/js/components/ProjectListItemPanel.jsx create mode 100644 app/static/app/js/css/Dashboard.scss delete mode 100644 app/static/app/js/index.jsx delete mode 100644 app/static/app/js/main.js create mode 100644 app/static/app/js/main.jsx create mode 100644 webpack-server.js diff --git a/Dockerfile b/Dockerfile index b3ec4dbd..d827fbe9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,8 +32,3 @@ RUN npm install WORKDIR /webodm RUN npm install -g webpack RUN npm install - -RUN webpack - -# Make sure all scripts are executable -RUN chmod +x *.sh diff --git a/README.md b/README.md index aa1605dd..5a3a145e 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,10 @@ Then: ``` pip install -r requirements.txt -pip install --upgrade git+git://github.com/Yelp/swagger_spec_validator git+https://github.com/pierotofy/django-knockout +pip install --upgrade git+git://github.com/Yelp/swagger_spec_validator +npm install -g webpack +npm install +webpack chmod +x start.sh && ./start.sh ``` diff --git a/app/api/projects.py b/app/api/projects.py index 1c143d57..5463a587 100644 --- a/app/api/projects.py +++ b/app/api/projects.py @@ -4,6 +4,7 @@ from app import models, permissions from guardian.shortcuts import get_objects_for_user class ProjectSerializer(serializers.HyperlinkedModelSerializer): + id = serializers.ReadOnlyField() owner = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) class Meta: diff --git a/app/static/app/bundles/.gitignore b/app/static/app/bundles/.gitignore index a6c7c285..f59ec20a 100644 --- a/app/static/app/bundles/.gitignore +++ b/app/static/app/bundles/.gitignore @@ -1 +1 @@ -*.js +* \ No newline at end of file diff --git a/app/static/app/css/main.css b/app/static/app/css/main.css deleted file mode 100644 index cd13e661..00000000 --- a/app/static/app/css/main.css +++ /dev/null @@ -1,55 +0,0 @@ -#navbar-top { - height: 50px; - min-height: 50px; - background-color: #18bc9c; } - #navbar-top .navbar-brand { - height: auto; - padding: 8px; } - #navbar-top .navbar-text { - margin-top: 14px; - margin-left: 4px; } - #navbar-top .navbar-link:hover { - color: white; - text-decoration: none; } - #navbar-top .navbar-top-links a.dropdown-toggle { - color: white; } - #navbar-top .navbar-top-links a:hover, #navbar-top .navbar-top-links a:focus, #navbar-top .navbar-top-links .open > a { - background-color: #2c3e50; } - #navbar-top ul#side-menu a:hover, #navbar-top ul#side-menu a:focus { - color: white; - text-decoration: none; - background-color: #2c3e50; } - #navbar-top ul#side-menu li { - word-break: break-word; } - #navbar-top .navbar-top-links li a.dropdown-toggle { - height: 50px; } - #navbar-top .user-profile { - padding: 0 8px; } - #navbar-top .user-profile .email { - font-size: 90%; - color: gray; } - -ul#side-menu.nav a { - color: #111; } - -.content { - clear: both; } - .content h1, .content h2, .content h3, .content h4, .content h5 { - padding-top: 4px; } - -.top-buffer { - margin-top: 15px; } - -.navbar-default { - background-color: white; } - -.alert { - margin-bottom: 10px; } - -table.table-first-col-bold td:first-child { - font-weight: bold; } - -button span.glyphicon { - margin-right: 4px; } - -/*# sourceMappingURL=main.css.map */ diff --git a/app/static/app/css/main.css.map b/app/static/app/css/main.css.map deleted file mode 100644 index 167109f5..00000000 --- a/app/static/app/css/main.css.map +++ /dev/null @@ -1,7 +0,0 @@ -{ -"version": 3, -"mappings": "AAAA,WAAW;EACP,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,IAAI;EAChB,gBAAgB,EAAE,OAAO;EAEzB,yBAAa;IACT,MAAM,EAAE,IAAI;IACZ,OAAO,EAAE,GAAG;EAEhB,wBAAY;IACR,UAAU,EAAE,IAAI;IAChB,WAAW,EAAE,GAAG;EAGpB,8BAAkB;IACd,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,IAAI;EAIrB,+CAAiB;IACb,KAAK,EAAE,KAAK;EAGhB,qHAAyB;IACrB,gBAAgB,EAAE,OAAO;EAM7B,kEAAgB;IACZ,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,IAAI;IACrB,gBAAgB,EAAE,OAAO;EAG7B,2BAAE;IACE,UAAU,EAAE,UAAU;EAI9B,kDAAsC;IAClC,MAAM,EAAE,IAAI;EAGhB,yBAAa;IACT,OAAO,EAAE,KAAK;IAEd,gCAAM;MACF,SAAS,EAAE,GAAG;MACd,KAAK,EAAE,IAAI;;AAMnB,kBAAC;EACG,KAAK,EAAE,IAAI;;AAInB,QAAQ;EACJ,KAAK,EAAE,IAAI;EACX,+DAAc;IACV,WAAW,EAAE,GAAG;;AAIxB,WAAY;EACR,UAAU,EAAE,IAAI;;AAGpB,eAAe;EACX,gBAAgB,EAAE,KAAK;;AAG3B,MAAM;EACF,aAAa,EAAE,IAAI;;AAInB,yCAAc;EACV,WAAW,EAAE,IAAI;;AAIzB,qBAAqB;EACjB,YAAY,EAAE,GAAG", -"sources": ["main.scss"], -"names": [], -"file": "main.css" -} \ No newline at end of file diff --git a/app/static/app/js/Dashboard.jsx b/app/static/app/js/Dashboard.jsx new file mode 100644 index 00000000..53468f57 --- /dev/null +++ b/app/static/app/js/Dashboard.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import './css/Dashboard.scss'; +import ProjectList from './components/ProjectList'; + +class Dashboard extends React.Component { + render() { + return ( + + ); + } +} + +export default Dashboard; diff --git a/app/static/app/js/app.jsx b/app/static/app/js/app.jsx deleted file mode 100644 index 79a3ba05..00000000 --- a/app/static/app/js/app.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -class App extends React.Component { - render() { - return ( -
Hello World!
- ); - } -} - -export default App; diff --git a/app/static/app/js/components/ProjectList.jsx b/app/static/app/js/components/ProjectList.jsx new file mode 100644 index 00000000..452765ad --- /dev/null +++ b/app/static/app/js/components/ProjectList.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import $ from 'jquery'; + +import ProjectListItem from './ProjectListItem'; + +class ProjectList extends React.Component { + constructor(){ + super(); + + this.state = { + loading: true, + error: "", + projects: null + } + } + + componentDidMount(){ + // Load projects from API + this.serverRequest = $.getJSON(this.props.source, json => { + if (json.results){ + this.setState({ + projects: json.results, + loading: false + }); + }else{ + this.setState({ + error: `Invalid JSON response: ${JSON.stringify(json)}`, + loading: false + }); + } + }) + .fail((jqXHR, textStatus, errorThrown) => { + this.setState({ + error: `Could not load projects list: ${textStatus}`, + loading: false + }); + }) + } + + componentWillUnmount(){ + this.serverRequest.abort(); + } + + render() { + if (this.state.loading){ + return (
Loading projects...
); + } + else if (this.state.projects){ + return (
+ {this.state.projects.map(p => ( + + ))} +
); + }else if (this.state.error){ + return (
An error occurred: {this.state.error}
); + }else{ + return (
); // should never happen + } + } +} + +export default ProjectList; diff --git a/app/static/app/js/components/ProjectListItem.jsx b/app/static/app/js/components/ProjectListItem.jsx new file mode 100644 index 00000000..2dc26c55 --- /dev/null +++ b/app/static/app/js/components/ProjectListItem.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import ProjectListItemPanel from './ProjectListItemPanel'; + +class ProjectListItem extends React.Component { + constructor(){ + super(); + this.state = { + collapsed: true + }; + + this.handleClick = this.handleClick.bind(this); + } + + handleClick(){ + this.state.collapsed = !this.state.collapsed; + } + + render() { + return ( +
+ {this.props.data.name} {this.props.data.created_at} + + {this.props.collapsed ? "" : } +
+ ); + } +} + +export default ProjectListItem; diff --git a/app/static/app/js/components/ProjectListItemPanel.jsx b/app/static/app/js/components/ProjectListItemPanel.jsx new file mode 100644 index 00000000..f4122340 --- /dev/null +++ b/app/static/app/js/components/ProjectListItemPanel.jsx @@ -0,0 +1,17 @@ +import React from 'react'; + +class ProjectListItemPanel extends React.Component { + constructor(){ + super(); + } + + render() { + return ( +
+ Sup! +
+ ); + } +} + +export default ProjectListItemPanel; diff --git a/app/static/app/js/css/Dashboard.scss b/app/static/app/js/css/Dashboard.scss new file mode 100644 index 00000000..0d005282 --- /dev/null +++ b/app/static/app/js/css/Dashboard.scss @@ -0,0 +1,3 @@ +#dashboard-app{ + background-color: yellow; +} \ No newline at end of file diff --git a/app/static/app/js/index.jsx b/app/static/app/js/index.jsx deleted file mode 100644 index 3b5773cb..00000000 --- a/app/static/app/js/index.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './app'; - -ReactDOM.render(, document.getElementById('react-app')) diff --git a/app/static/app/js/main.js b/app/static/app/js/main.js deleted file mode 100644 index 61c4d351..00000000 --- a/app/static/app/js/main.js +++ /dev/null @@ -1,38 +0,0 @@ -$(function(){ - console.log("Hello!"); - $('#side-menu').metisMenu(); - - $(window).bind("load resize", function() { - var topOffset = 50; - var width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width; - if (width < 768) { - $('div.navbar-collapse').addClass('collapse'); - topOffset = 100; // 2-row-menu - } else { - $('div.navbar-collapse').removeClass('collapse'); - } - - var height = ((this.window.innerHeight > 0) ? this.window.innerHeight : this.screen.height) - 1; - height = height - topOffset; - if (height < 1) height = 1; - if (height > topOffset) { - $("#page-wrapper").css("min-height", (height) + "px"); - } - }); - - var url = window.location; - // var element = $('ul.nav a').filter(function() { - // return this.href == url; - // }).addClass('active').parent().parent().addClass('in').parent(); - var element = $('ul.nav a').filter(function() { - return this.href == url; - }).addClass('active').parent(); - - while(true){ - if (element.is('li')){ - element = element.parent().addClass('in').parent(); - } else { - break; - } - } -}); diff --git a/app/static/app/js/main.jsx b/app/static/app/js/main.jsx new file mode 100644 index 00000000..383668a6 --- /dev/null +++ b/app/static/app/js/main.jsx @@ -0,0 +1,6 @@ +import '../css/main.scss'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import Dashboard from './Dashboard'; + +ReactDOM.render(, document.getElementById('dashboard-app')); diff --git a/app/templates/app/base.html b/app/templates/app/base.html index 5bf688e0..eb6d7602 100644 --- a/app/templates/app/base.html +++ b/app/templates/app/base.html @@ -22,9 +22,8 @@ {% block extra-headers %}{% endblock %} - + - {{title|default:"Login"}} - WebODM @@ -71,5 +70,44 @@ - + \ No newline at end of file diff --git a/app/templates/app/dashboard.html b/app/templates/app/dashboard.html index a96841df..9bd2f80f 100644 --- a/app/templates/app/dashboard.html +++ b/app/templates/app/dashboard.html @@ -37,6 +37,6 @@ {% endif %} -
+
{% render_bundle 'main' %} {% endblock %} diff --git a/docker-compose.yml b/docker-compose.yml index b6a61b3d..02bbc673 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: volumes: - .:/webodm - /webodm/nodeodm/external/node-OpenDroneMap/node_modules + - /webodm/node_modules ports: - "8000:8000" depends_on: diff --git a/package.json b/package.json index 81d7f04f..b07602da 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,18 @@ "babel-loader": "^6.2.5", "babel-preset-es2015": "^6.16.0", "babel-preset-react": "^6.16.0", + "css-loader": "^0.25.0", + "extract-text-webpack-plugin": "^1.0.1", + "file-loader": "^0.9.0", + "node-sass": "^3.10.1", "react": "^15.3.2", "react-dom": "^15.3.2", + "react-hot-loader": "^3.0.0-beta.5", + "sass-loader": "^4.0.2", + "style-loader": "^0.13.1", "webpack": "^1.13.2", - "webpack-bundle-tracker": "0.0.93" + "webpack-bundle-tracker": "0.0.93", + "webpack-dev-server": "^1.16.2", + "webpack-livereload-plugin": "^0.9.0" } } diff --git a/start.sh b/start.sh index cd72d1f6..8b1a8516 100644 --- a/start.sh +++ b/start.sh @@ -1,4 +1,7 @@ #!/bin/bash +echo Building asssets... +webpack + echo Running migrations python manage.py makemigrations python manage.py migrate diff --git a/webpack-server.js b/webpack-server.js new file mode 100644 index 00000000..274c5aba --- /dev/null +++ b/webpack-server.js @@ -0,0 +1,15 @@ +let webpack = require('webpack'); +let WebpackDevServer = require('webpack-dev-server'); +let config = require('./webpack.config'); + +new WebpackDevServer(webpack(config), { + publicPath: config.output.publicPath, + hot: true, + inline: true +}).listen(3000, '0.0.0.0', (err, result) => { + if (err) { + console.log(err); + } + + console.log('Listening at 0.0.0.0:3000'); +}) \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 80fc8e1c..19a0aece 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,19 +1,32 @@ -var path = require("path") -var webpack = require('webpack') -var BundleTracker = require('webpack-bundle-tracker') +let path = require("path"); +let webpack = require('webpack'); +let BundleTracker = require('webpack-bundle-tracker'); +let ExtractTextPlugin = require('extract-text-webpack-plugin'); +let LiveReloadPlugin = require('webpack-livereload-plugin'); module.exports = { context: __dirname, - entry: './app/static/app/js/index.jsx', // entry point of our app. + entry: [ + // 'webpack-dev-server/client?http://localhost:3000', + // 'webpack/hot/only-dev-server', + './app/static/app/js/main.jsx', + ], output: { - path: path.resolve('./app/static/app/bundles/'), - filename: "[name]-[hash].js", + path: path.join(__dirname, './app/static/app/bundles/'), + filename: "[name]-[hash].js" + // publicPath: 'http://localhost:3000/app/static/app/bundles/', // Tell django to use this URL to load packages and not use STATIC_URL + bundle_name }, plugins: [ + // new webpack.HotModuleReplacementPlugin(), + // new webpack.NoErrorsPlugin(), // don't reload if there is an error + new LiveReloadPlugin(), new BundleTracker({filename: './webpack-stats.json'}), + new ExtractTextPlugin('css/main.css', { + allChunks: true + }) ], module: { @@ -23,8 +36,13 @@ module.exports = { exclude: /(node_modules|bower_components)/, loader: 'babel-loader', query: { + // plugins: ['react-hot-loader/babel'], presets: ['es2015', 'react'] } + }, + { + test: /\.scss$/, + loader: ExtractTextPlugin.extract('css!sass') } ] }, @@ -32,5 +50,11 @@ module.exports = { resolve: { modulesDirectories: ['node_modules', 'bower_components'], extensions: ['', '.js', '.jsx'] + }, + + externals: { + // require("jquery") is external and available + // on the global let jQuery + "jquery": "jQuery" } } \ No newline at end of file