Added docker-compose support
This commit is contained in:
parent
79ff1e8ff9
commit
9be28477ed
31
Dockerfile
31
Dockerfile
@ -1,13 +1,32 @@
|
||||
FROM node:alpine as builder
|
||||
|
||||
RUN apk add yarn \
|
||||
python \
|
||||
make \
|
||||
g++
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
COPY package.json yarn.lock /app/
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY tslint.json /app
|
||||
COPY app/ /app/app
|
||||
COPY common/ /app/common
|
||||
COPY server/ /app/server
|
||||
|
||||
RUN yarn build
|
||||
|
||||
RUN yarn install --frozen-lockfile --production
|
||||
|
||||
FROM node:alpine
|
||||
|
||||
RUN npm install --global yarn
|
||||
|
||||
ADD package.json yarn.lock /app/
|
||||
WORKDIR /app/
|
||||
RUN yarn install --production
|
||||
|
||||
ADD dist/ /app/dist
|
||||
ADD public/ /app/public
|
||||
COPY --from=builder /app/package.json /app/yarn.lock ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT [ "npm", "run", "start" ]
|
||||
|
@ -2,7 +2,7 @@ import { observer } from "mobx-react";
|
||||
import { createViewModel, IViewModel } from "mobx-utils";
|
||||
import * as React from "react";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { Button, CheckboxProps, Form, Input, InputOnChangeData, Menu, Modal, Segment } from "semantic-ui-react";
|
||||
import { Button, CheckboxProps, Form, Input, InputOnChangeData, Menu, Modal } from "semantic-ui-react";
|
||||
|
||||
import { ProgramSequenceView, ScheduleView } from "@app/components";
|
||||
import * as rp from "@app/routePaths";
|
||||
|
@ -11,8 +11,8 @@ const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const HappyPack = require("happypack");
|
||||
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
|
||||
|
||||
const {getClientEnvironment} = require("../env");
|
||||
const paths = require("../paths");
|
||||
const {getClientEnvironment} = require("../common/env");
|
||||
const paths = require("../common/paths");
|
||||
|
||||
// Webpack uses `publicPath` to determine where the app is being served from.
|
||||
// It requires a trailing slash, or the file assets will get an incorrect path.
|
||||
|
26
build.sh
Executable file
26
build.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
IMAGE_NAME="amikhalev/sprinklers3"
|
||||
BUILD_IMAGE="$IMAGE_NAME:build"
|
||||
DIST_IMAGE="$IMAGE_NAME:dist"
|
||||
EXTRACT_CONTAINER="extract-$RANDOM"
|
||||
BUILD_DIR="."
|
||||
|
||||
echo "Cleaning build files"
|
||||
rm -rf ./build ./dist ./public
|
||||
|
||||
echo "Building build image $BUILD_IMAGE"
|
||||
docker build -t "$BUILD_IMAGE" --target builder .
|
||||
|
||||
echo "Extracting build image using container $EXTRACT_CONTAINER"
|
||||
mkdir -p ./build
|
||||
cp package.json yarn.lock "$BUILD_DIR"
|
||||
docker container create --name "$EXTRACT_CONTAINER" "$BUILD_IMAGE"
|
||||
docker container cp "$EXTRACT_CONTAINER:/app/dist" "$BUILD_DIR/dist"
|
||||
docker container cp "$EXTRACT_CONTAINER:/app/public" "$BUILD_DIR/public"
|
||||
docker container rm -f "$EXTRACT_CONTAINER"
|
||||
|
||||
echo "Building dist image $DIST_IMAGE"
|
||||
docker build -t "$DIST_IMAGE" .
|
@ -1,4 +1,8 @@
|
||||
export class Duration {
|
||||
static fromSeconds(seconds: number): Duration {
|
||||
return new Duration(Math.floor(seconds / 60), seconds % 60);
|
||||
}
|
||||
|
||||
minutes: number = 0;
|
||||
seconds: number = 0;
|
||||
|
||||
@ -7,10 +11,6 @@ export class Duration {
|
||||
this.seconds = seconds;
|
||||
}
|
||||
|
||||
static fromSeconds(seconds: number): Duration {
|
||||
return new Duration(Math.floor(seconds / 60), seconds % 60);
|
||||
}
|
||||
|
||||
toSeconds(): number {
|
||||
return this.minutes * 60 + this.seconds;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ if (!NODE_ENV) {
|
||||
}
|
||||
|
||||
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
|
||||
var dotenvFiles = [
|
||||
const dotenvFiles = [
|
||||
`${paths.dotenv}.${NODE_ENV}.local`,
|
||||
`${paths.dotenv}.${NODE_ENV}`,
|
||||
// Don"t include `.env.local` for `test` environment
|
@ -1,5 +1,7 @@
|
||||
import * as pino from "pino";
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
type Level = "default" | "60" | "50" | "40" | "30" | "20" | "10";
|
||||
|
||||
const levels: {[level in Level]: string } = {
|
||||
|
@ -19,7 +19,7 @@ export abstract class SprinklersDevice {
|
||||
sectionRunnerConstructor: typeof SectionRunner = SectionRunner;
|
||||
programConstructor: typeof Program = Program;
|
||||
|
||||
constructor() {
|
||||
protected constructor() {
|
||||
this.sectionRunner = new (this.sectionRunnerConstructor)(this);
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ export type UpdateProgramResponse = Response<"updateProgram", { data: any }>;
|
||||
export interface WithSection { sectionId: number; }
|
||||
|
||||
export type RunSectionData = WithSection & { duration: number };
|
||||
export type RunSectionReqeust = RunSectionData & WithType<"runSection">;
|
||||
export type RunSectionRequest = RunSectionData & WithType<"runSection">;
|
||||
export type RunSectionResponse = Response<"runSection", { runId: number }>;
|
||||
|
||||
export type CancelSectionRequest = WithSection & WithType<"cancelSection">;
|
||||
@ -26,7 +26,7 @@ export interface PauseSectionRunnerData { paused: boolean; }
|
||||
export type PauseSectionRunnerRequest = PauseSectionRunnerData & WithType<"pauseSectionRunner">;
|
||||
|
||||
export type Request = RunProgramRequest | CancelProgramRequest | UpdateProgramRequest |
|
||||
RunSectionReqeust | CancelSectionRequest | CancelSectionRunIdRequest | PauseSectionRunnerRequest;
|
||||
RunSectionRequest | CancelSectionRequest | CancelSectionRunIdRequest | PauseSectionRunnerRequest;
|
||||
|
||||
export type RequestType = Request["type"];
|
||||
|
||||
|
18
common/sprinklersRpc/mqtt/MqttProgram.ts
Normal file
18
common/sprinklersRpc/mqtt/MqttProgram.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { update } from "serializr";
|
||||
|
||||
import * as s from "@common/sprinklersRpc";
|
||||
import * as schema from "@common/sprinklersRpc/schema";
|
||||
|
||||
export class MqttProgram extends s.Program {
|
||||
onMessage(payload: string, topic: string | undefined) {
|
||||
if (topic === "running") {
|
||||
this.running = (payload === "true");
|
||||
} else if (topic == null) {
|
||||
this.updateFromJSON(JSON.parse(payload));
|
||||
}
|
||||
}
|
||||
|
||||
updateFromJSON(json: any) {
|
||||
update(schema.program, this, json);
|
||||
}
|
||||
}
|
18
common/sprinklersRpc/mqtt/MqttSection.ts
Normal file
18
common/sprinklersRpc/mqtt/MqttSection.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { update } from "serializr";
|
||||
|
||||
import * as s from "@common/sprinklersRpc";
|
||||
import * as schema from "@common/sprinklersRpc/schema";
|
||||
|
||||
export class MqttSection extends s.Section {
|
||||
onMessage(payload: string, topic: string | undefined) {
|
||||
if (topic === "state") {
|
||||
this.state = (payload === "true");
|
||||
} else if (topic == null) {
|
||||
this.updateFromJSON(JSON.parse(payload));
|
||||
}
|
||||
}
|
||||
|
||||
updateFromJSON(json: any) {
|
||||
update(schema.section, this, json);
|
||||
}
|
||||
}
|
14
common/sprinklersRpc/mqtt/MqttSectionRunner.ts
Normal file
14
common/sprinklersRpc/mqtt/MqttSectionRunner.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { update } from "serializr";
|
||||
|
||||
import * as s from "@common/sprinklersRpc";
|
||||
import * as schema from "@common/sprinklersRpc/schema";
|
||||
|
||||
export class MqttSectionRunner extends s.SectionRunner {
|
||||
onMessage(payload: string) {
|
||||
this.updateFromJSON(JSON.parse(payload));
|
||||
}
|
||||
|
||||
updateFromJSON(json: any) {
|
||||
update(schema.sectionRunner, this, json);
|
||||
}
|
||||
}
|
@ -1,14 +1,16 @@
|
||||
import { autorun, observable } from "mobx";
|
||||
import * as mqtt from "mqtt";
|
||||
import { update } from "serializr";
|
||||
|
||||
import logger from "@common/logger";
|
||||
import * as s from "@common/sprinklersRpc";
|
||||
import * as requests from "@common/sprinklersRpc/deviceRequests";
|
||||
import * as schema from "@common/sprinklersRpc/schema";
|
||||
import { seralizeRequest } from "@common/sprinklersRpc/schema/requests";
|
||||
import { getRandomId } from "@common/utils";
|
||||
|
||||
import { MqttProgram } from "./MqttProgram";
|
||||
import { MqttSection } from "./MqttSection";
|
||||
import { MqttSectionRunner } from "./MqttSectionRunner";
|
||||
|
||||
const log = logger.child({ source: "mqtt" });
|
||||
|
||||
interface WithRid {
|
||||
@ -16,24 +18,24 @@ interface WithRid {
|
||||
}
|
||||
|
||||
export class MqttRpcClient implements s.SprinklersRPC {
|
||||
get connected(): boolean {
|
||||
return this.connectionState.isServerConnected || false;
|
||||
}
|
||||
|
||||
private static newClientId() {
|
||||
return "sprinklers3-MqttApiClient-" + getRandomId();
|
||||
}
|
||||
|
||||
readonly mqttUri: string;
|
||||
client!: mqtt.Client;
|
||||
@observable connectionState: s.ConnectionState = new s.ConnectionState();
|
||||
devices: Map<string, MqttSprinklersDevice> = new Map();
|
||||
|
||||
get connected(): boolean {
|
||||
return this.connectionState.isServerConnected || false;
|
||||
}
|
||||
|
||||
constructor(mqttUri: string) {
|
||||
this.mqttUri = mqttUri;
|
||||
this.connectionState.serverToBroker = false;
|
||||
}
|
||||
|
||||
private static newClientId() {
|
||||
return "sprinklers3-MqttApiClient-" + getRandomId();
|
||||
}
|
||||
|
||||
start() {
|
||||
const clientId = MqttRpcClient.newClientId();
|
||||
log.info({ mqttUri: this.mqttUri, clientId }, "connecting to mqtt broker with client id");
|
||||
@ -281,41 +283,3 @@ class MqttSprinklersDevice extends s.SprinklersDevice {
|
||||
}
|
||||
|
||||
type ResponseCallback = (response: requests.Response) => void;
|
||||
|
||||
class MqttSection extends s.Section {
|
||||
onMessage(payload: string, topic: string | undefined) {
|
||||
if (topic === "state") {
|
||||
this.state = (payload === "true");
|
||||
} else if (topic == null) {
|
||||
this.updateFromJSON(JSON.parse(payload));
|
||||
}
|
||||
}
|
||||
|
||||
updateFromJSON(json: any) {
|
||||
update(schema.section, this, json);
|
||||
}
|
||||
}
|
||||
|
||||
class MqttProgram extends s.Program {
|
||||
onMessage(payload: string, topic: string | undefined) {
|
||||
if (topic === "running") {
|
||||
this.running = (payload === "true");
|
||||
} else if (topic == null) {
|
||||
this.updateFromJSON(JSON.parse(payload));
|
||||
}
|
||||
}
|
||||
|
||||
updateFromJSON(json: any) {
|
||||
update(schema.program, this, json);
|
||||
}
|
||||
}
|
||||
|
||||
class MqttSectionRunner extends s.SectionRunner {
|
||||
onMessage(payload: string) {
|
||||
this.updateFromJSON(JSON.parse(payload));
|
||||
}
|
||||
|
||||
updateFromJSON(json: any) {
|
||||
update(schema.sectionRunner, this, json);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { observable } from "mobx";
|
||||
|
||||
export class TimeOfDay {
|
||||
static fromDate(date: Date): TimeOfDay {
|
||||
return new TimeOfDay(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
|
||||
}
|
||||
|
||||
readonly hour: number;
|
||||
readonly minute: number;
|
||||
readonly second: number;
|
||||
@ -12,10 +16,6 @@ export class TimeOfDay {
|
||||
this.second = second;
|
||||
this.millisecond = millisecond;
|
||||
}
|
||||
|
||||
static fromDate(date: Date): TimeOfDay {
|
||||
return new TimeOfDay(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
|
||||
}
|
||||
}
|
||||
|
||||
export enum Weekday {
|
||||
|
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@ -0,0 +1,22 @@
|
||||
version: "3"
|
||||
services:
|
||||
web:
|
||||
image: "amikhalev/sprinklers3"
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- PORT=8080
|
||||
- TYPEORM_CONNECTION=postgres
|
||||
- TYPEORM_HOST=database
|
||||
- TYPEORM_DATABASE=postgres
|
||||
- TYPEORM_USERNAME=postgres
|
||||
- TYPEORM_PASSWORD=8JN4w0UsN5dbjMjNvPe452P2yYOqg5PV
|
||||
# Must specify JWT_SECRET and MQTT_URL
|
||||
|
||||
database:
|
||||
image: "postgres:11-alpine"
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=8JN4w0UsN5dbjMjNvPe452P2yYOqg5PV
|
@ -8,6 +8,7 @@
|
||||
"clean": "rm -rf ./dist ./build ./public",
|
||||
"build:app": "NODE_ENV=production webpack --config ./app/webpack.config.js --env prod",
|
||||
"build:server": "tsc --project server",
|
||||
"build": "run-p build:*",
|
||||
"watch:app": "yarn build:app --watch",
|
||||
"watch:server": "yarn build:server --watch",
|
||||
"start:dev-server": "NODE_ENV=development webpack-dev-server --config ./app/webpack.config.js --env dev",
|
||||
@ -41,7 +42,6 @@
|
||||
"express": "^4.16.3",
|
||||
"express-pino-logger": "^3.0.2",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.3",
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"lodash": "^4.17.10",
|
||||
"mobx": "^5.0.3",
|
||||
@ -54,7 +54,6 @@
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"serializr": "^1.3.0",
|
||||
"typeorm": "^0.2.7",
|
||||
"uglify-es": "3.3.9",
|
||||
"ws": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -84,6 +83,7 @@
|
||||
"favicons-webpack-plugin": "^0.0.9",
|
||||
"file-loader": "^1.1.11",
|
||||
"font-awesome": "^4.7.0",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.3",
|
||||
"happypack": "^5.0.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.4.1",
|
||||
@ -114,6 +114,7 @@
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-react": "^3.6.0",
|
||||
"typescript": "^2.9.2",
|
||||
"uglify-es": "^3.3.9",
|
||||
"uglifyjs-webpack-plugin": "^1.2.6",
|
||||
"url-loader": "^1.0.1",
|
||||
"webpack": "^4.16.2",
|
||||
|
@ -2,5 +2,3 @@ import * as moduleAlias from "module-alias";
|
||||
import * as path from "path";
|
||||
moduleAlias.addAlias("@common", path.resolve(__dirname, "..", "common"));
|
||||
moduleAlias.addAlias("@server", __dirname);
|
||||
moduleAlias.addAlias("env", require.resolve("../env"));
|
||||
moduleAlias.addAlias("paths", require.resolve("../paths"));
|
||||
|
@ -11,4 +11,4 @@ const errorHandler: express.ErrorRequestHandler =
|
||||
}
|
||||
};
|
||||
|
||||
export default errorHandler;
|
||||
export default errorHandler;
|
||||
|
@ -2,7 +2,7 @@ import { Express } from "express";
|
||||
import * as path from "path";
|
||||
import * as serveStatic from "serve-static";
|
||||
|
||||
import * as paths from "paths";
|
||||
import * as paths from "@common/paths";
|
||||
|
||||
const index = path.join(paths.publicDir, "index.html");
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
/* tslint:disable:ordered-imports */
|
||||
import "reflect-metadata";
|
||||
import "./configureAlias";
|
||||
import "env";
|
||||
import "@common/env";
|
||||
import "./configureLogger";
|
||||
|
||||
import log from "@common/logger";
|
||||
|
@ -9,7 +9,7 @@
|
||||
true
|
||||
],
|
||||
"max-classes-per-file": [
|
||||
false
|
||||
true, 3
|
||||
],
|
||||
"ordered-imports": true,
|
||||
"variable-name": [
|
||||
@ -24,16 +24,13 @@
|
||||
"member-ordering": [
|
||||
true,
|
||||
{
|
||||
"order": "fields-first"
|
||||
"order": "statics-first"
|
||||
}
|
||||
],
|
||||
"object-literal-sort-keys": [
|
||||
false
|
||||
],
|
||||
"no-submodule-imports": false,
|
||||
"no-unused-variable": [
|
||||
true
|
||||
],
|
||||
"jsx-boolean-value": [ true, "never" ],
|
||||
"no-implicit-dependencies": false
|
||||
},
|
||||
|
@ -7125,7 +7125,7 @@ ua-parser-js@^0.7.18:
|
||||
version "0.7.18"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
|
||||
|
||||
uglify-es@3.3.9, uglify-es@^3.3.4:
|
||||
uglify-es@^3.3.4, uglify-es@^3.3.9:
|
||||
version "3.3.9"
|
||||
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
|
||||
dependencies:
|
||||
|
Loading…
x
Reference in New Issue
Block a user