├── test ├── test_helper.exs ├── hello_web │ ├── views │ │ ├── page_view_test.exs │ │ ├── layout_view_test.exs │ │ └── error_view_test.exs │ └── controllers │ │ └── page_controller_test.exs └── support │ ├── channel_case.ex │ └── conn_case.ex ├── rel ├── vm.args └── config.exs ├── lib ├── hello_web │ ├── templates │ │ ├── page │ │ │ └── index.html.eex │ │ └── layout │ │ │ └── app.html.eex │ ├── views │ │ ├── layout_view.ex │ │ ├── page_view.ex │ │ ├── error_view.ex │ │ └── error_helpers.ex │ ├── controllers │ │ └── page_controller.ex │ ├── router.ex │ ├── channels │ │ ├── demo_channel.ex │ │ └── user_socket.ex │ ├── gettext.ex │ └── endpoint.ex ├── hello.ex ├── hello │ ├── cards.ex │ └── application.ex └── hello_web.ex ├── assets ├── static │ ├── favicon.ico │ ├── images │ │ └── phoenix.png │ └── robots.txt ├── package.json ├── brunch-config.js ├── css │ └── app.css ├── js │ └── app.js └── package-lock.json ├── apply-hello ├── config ├── test.exs ├── config.exs ├── dev.exs └── prod.exs ├── .dockerignore ├── priv └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot ├── .gitignore ├── deploy ├── cloudbuild.yml └── k8s_template.yml ├── k8s └── hello.yaml ├── mix.exs ├── Dockerfile ├── mix.lock └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | -------------------------------------------------------------------------------- /rel/vm.args: -------------------------------------------------------------------------------- 1 | -name hello@${MY_POD_IP} 2 | -setcookie hellocookie 3 | -------------------------------------------------------------------------------- /lib/hello_web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 | -------------------------------------------------------------------------------- /lib/hello_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.LayoutView do 2 | use HelloWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/hello_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.PageView do 2 | use HelloWeb, :view 3 | 4 | end 5 | -------------------------------------------------------------------------------- /assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamgriffiths/distributed-elixir-demo/HEAD/assets/static/favicon.ico -------------------------------------------------------------------------------- /test/hello_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.PageViewTest do 2 | use HelloWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /apply-hello: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # apply all the kubernetes configs for the hello app 4 | 5 | kubectl apply -f ./k8s/hello.yaml 6 | -------------------------------------------------------------------------------- /test/hello_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.LayoutViewTest do 2 | use HelloWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /assets/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamgriffiths/distributed-elixir-demo/HEAD/assets/static/images/phoenix.png -------------------------------------------------------------------------------- /lib/hello_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.PageController do 2 | use HelloWeb, :controller 3 | 4 | def index(conn, _params) do 5 | render conn, "index.html" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /assets/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /test/hello_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.PageControllerTest do 2 | use HelloWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get conn, "/" 6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/hello.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello do 2 | @moduledoc """ 3 | Hello keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :hello, HelloWeb.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore the following directories when doing a Docker build. 2 | # These directories will contain artifacts for local (not-in-Docker) builds, 3 | # and it is best to omit them to ensure a clean build in Docker. 4 | 5 | # Note: If you are using an umbrella app, you might need to add more 6 | # directories to this list. 7 | 8 | _build 9 | deps 10 | test 11 | assets/node_modules 12 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | -------------------------------------------------------------------------------- /test/hello_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.ErrorViewTest do 2 | use HelloWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(HelloWeb.ErrorView, "404.html", []) == 9 | "Not Found" 10 | end 11 | 12 | test "renders 500.html" do 13 | assert render_to_string(HelloWeb.ErrorView, "500.html", []) == 14 | "Internal Server Error" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/hello_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.ErrorView do 2 | use HelloWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/hello_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.Router do 2 | use HelloWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_flash 8 | plug :protect_from_forgery 9 | plug :put_secure_browser_headers 10 | end 11 | 12 | pipeline :api do 13 | plug :accepts, ["json"] 14 | end 15 | 16 | scope "/", HelloWeb do 17 | pipe_through :browser # Use the default browser stack 18 | 19 | get "/", PageController, :index 20 | end 21 | 22 | # Other scopes may use custom stacks. 23 | # scope "/api", HelloWeb do 24 | # pipe_through :api 25 | # end 26 | end 27 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "license": "MIT", 4 | "scripts": { 5 | "deploy": "brunch build --production", 6 | "watch": "brunch watch --stdin" 7 | }, 8 | "dependencies": { 9 | "phoenix": "file:../deps/phoenix", 10 | "phoenix_html": "file:../deps/phoenix_html", 11 | "react": "^16.2.0", 12 | "react-dom": "^16.2.0" 13 | }, 14 | "devDependencies": { 15 | "babel": "^6.23.0", 16 | "babel-brunch": "6.1.1", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "brunch": "2.10.9", 20 | "clean-css-brunch": "2.10.0", 21 | "uglify-js-brunch": "2.10.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/hello/cards.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.Cards do 2 | 3 | @size 5 * 5 4 | 5 | def initial do 6 | List.duplicate("222222", @size) 7 | end 8 | 9 | def start_link(state) do 10 | case Agent.start_link(fn -> state end, name: {:global, __MODULE__}) do 11 | {:ok, pid} -> 12 | {:ok, pid} 13 | {:error, {:already_started, pid}} -> 14 | {:ok, pid} 15 | end 16 | end 17 | 18 | def get do 19 | Agent.get({:global, __MODULE__}, fn state -> state end) 20 | end 21 | 22 | def update(index, next) do 23 | Agent.update({:global, __MODULE__}, fn state -> 24 | List.replace_at(state, index, next) 25 | end) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/hello_web/channels/demo_channel.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.DemoChannel do 2 | use Phoenix.Channel 3 | require Logger 4 | 5 | def join("demo:hello", _message, socket) do 6 | send(self(), {:new_connection, %{}}) 7 | {:ok, socket} 8 | end 9 | 10 | def handle_info({:new_connection, _msg}, socket) do 11 | cards = Hello.Cards.get() 12 | push(socket, "cards:state", %{data: cards}) 13 | {:noreply, socket} 14 | end 15 | 16 | def handle_in("cards:change", %{"index" => index, "color" => color}, socket) do 17 | Hello.Cards.update(index, color) 18 | broadcast!(socket, "cards:change", %{ index: index, color: color }) 19 | {:noreply, socket} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/hello_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | hello hello 11 | "> 12 | 13 | 14 | 15 |
16 | <%= render @view_module, @view_template, assigns %> 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # Generated on crash by NPM 11 | npm-debug.log 12 | 13 | # Static artifacts 14 | /assets/node_modules 15 | 16 | # Since we are building assets from assets/, 17 | # we ignore priv/static. You may want to comment 18 | # this depending on your deployment strategy. 19 | /priv/static/ 20 | 21 | # Files matching config/*.secret.exs pattern contain sensitive 22 | # data and you should not commit them into version control. 23 | # 24 | # Alternatively, you may comment the line below and commit the 25 | # secrets files as long as you replace their contents by environment 26 | # variables. 27 | /config/*.secret.exs -------------------------------------------------------------------------------- /deploy/cloudbuild.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: 'gcr.io/cloud-builders/docker' 3 | args: ["build", "-t", "gcr.io/distributed-elixir-demo/hello:$REVISION_ID", "."] 4 | 5 | - name: 'gcr.io/cloud-builders/docker' 6 | args: ["push", "gcr.io/distributed-elixir-demo/hello:$REVISION_ID"] 7 | 8 | - name: 'alpine' 9 | entrypoint: 'sh' 10 | args: 11 | - '-c' 12 | - | 13 | (echo "cat < k8s.yml 14 | 15 | - name: 'gcr.io/cloud-builders/kubectl' 16 | args: ["apply", "-f", "k8s.yml"] 17 | env: 18 | - 'CLOUDSDK_COMPUTE_ZONE=us-central1-a' 19 | - 'CLOUDSDK_CONTAINER_CLUSTER=hello-cluster' 20 | -------------------------------------------------------------------------------- /lib/hello_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import HelloWeb.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :hello 24 | end 25 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | # The default endpoint for testing 24 | @endpoint HelloWeb.Endpoint 25 | end 26 | end 27 | 28 | 29 | setup _tags do 30 | :ok 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # Configures the endpoint 9 | config :hello, HelloWeb.Endpoint, 10 | url: [host: "localhost"], 11 | secret_key_base: "ADCarnF2URllg1IvUf5P/DROZlAvoX/nfx394TmC9qXqurS8J116sUtpSrTikmU7", 12 | render_errors: [view: HelloWeb.ErrorView, accepts: ~w(html json)], 13 | pubsub: [name: Hello.PubSub, 14 | adapter: Phoenix.PubSub.PG2] 15 | 16 | # Configures Elixir's Logger 17 | config :logger, :console, 18 | format: "$time $metadata[$level] $message\n", 19 | metadata: [:user_id] 20 | 21 | # Import environment specific config. This must remain at the bottom 22 | # of this file so it overrides the configuration defined above. 23 | import_config "#{Mix.env}.exs" 24 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | import HelloWeb.Router.Helpers 23 | 24 | # The default endpoint for testing 25 | @endpoint HelloWeb.Endpoint 26 | end 27 | end 28 | 29 | 30 | setup _tags do 31 | {:ok, conn: Phoenix.ConnTest.build_conn()} 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /deploy/k8s_template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: hello 5 | --- 6 | apiVersion: extensions/v1beta1 7 | kind: Deployment 8 | metadata: 9 | name: app 10 | namespace: hello 11 | spec: 12 | replicas: 2 13 | template: 14 | metadata: 15 | labels: 16 | app: hello 17 | spec: 18 | containers: 19 | - name: hello 20 | image: $IMAGE 21 | ports: 22 | - containerPort: 8080 23 | env: 24 | - name: MY_POD_IP 25 | valueFrom: 26 | fieldRef: 27 | fieldPath: status.podIP 28 | --- 29 | apiVersion: v1 30 | kind: Service 31 | metadata: 32 | name: web 33 | namespace: hello 34 | spec: 35 | ports: 36 | - port: 80 37 | targetPort: 8080 38 | protocol: TCP 39 | name: http 40 | selector: 41 | app: hello 42 | type: LoadBalancer 43 | --- 44 | apiVersion: v1 45 | kind: Service 46 | metadata: 47 | name: headless-service 48 | namespace: hello 49 | spec: 50 | ports: 51 | - port: 7777 52 | selector: 53 | app: hello 54 | clusterIP: None 55 | -------------------------------------------------------------------------------- /lib/hello/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Hello.Application do 2 | use Application 3 | 4 | # See https://hexdocs.pm/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | # Start the endpoint when the application starts 12 | supervisor(HelloWeb.Endpoint, []), 13 | # Start your own worker by calling: Hello.Worker.start_link(arg0, arg2, arg3) 14 | # worker(Hello.Worker, [arg1, arg2, arg3]), 15 | worker(Hello.Cards, [Hello.Cards.initial()]) 16 | ] 17 | 18 | # See https://hexdocs.pm/elixir/Supervisor.html 19 | # for other strategies and supported options 20 | opts = [strategy: :one_for_one, name: Hello.Supervisor] 21 | Supervisor.start_link(children, opts) 22 | end 23 | 24 | # Tell Phoenix to update the endpoint configuration 25 | # whenever the application is updated. 26 | def config_change(changed, _new, removed) do 27 | HelloWeb.Endpoint.config_change(changed, removed) 28 | :ok 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /k8s/hello.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: hello 5 | --- 6 | apiVersion: extensions/v1beta1 7 | kind: Deployment 8 | metadata: 9 | name: app 10 | namespace: hello 11 | spec: 12 | replicas: 2 13 | template: 14 | metadata: 15 | labels: 16 | app: hello 17 | spec: 18 | containers: 19 | - name: hello 20 | image: liamgriffiths/hello:18 21 | ports: 22 | - containerPort: 8080 23 | env: 24 | - name: MY_POD_IP 25 | valueFrom: 26 | fieldRef: 27 | fieldPath: status.podIP 28 | --- 29 | apiVersion: v1 30 | kind: Service 31 | metadata: 32 | name: web 33 | namespace: hello 34 | spec: 35 | ports: 36 | - port: 6000 37 | targetPort: 8080 38 | protocol: TCP 39 | name: http 40 | selector: 41 | app: hello 42 | type: LoadBalancer 43 | --- 44 | apiVersion: v1 45 | kind: Service 46 | metadata: 47 | name: headless-service 48 | namespace: hello 49 | spec: 50 | ports: 51 | - port: 7777 52 | selector: 53 | app: hello 54 | clusterIP: None 55 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Hello.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :hello, 7 | version: "0.0.1", 8 | elixir: "~> 1.4", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: [:phoenix, :gettext] ++ Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps() 13 | ] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [ 21 | mod: {Hello.Application, []}, 22 | extra_applications: [:logger, :runtime_tools, :peerage] 23 | ] 24 | end 25 | 26 | # Specifies which paths to compile per environment. 27 | defp elixirc_paths(:test), do: ["lib", "test/support"] 28 | defp elixirc_paths(_), do: ["lib"] 29 | 30 | # Specifies your project dependencies. 31 | # 32 | # Type `mix help deps` for examples and options. 33 | defp deps do 34 | [ 35 | {:phoenix, "~> 1.3.2"}, 36 | {:phoenix_pubsub, "~> 1.0"}, 37 | {:phoenix_html, "~> 2.10"}, 38 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 39 | {:gettext, "~> 0.11"}, 40 | {:cowboy, "~> 1.0"}, 41 | {:distillery, "~> 1.5"}, 42 | {:peerage, "~> 1.0.2"} 43 | ] 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/hello_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | channel "demo:*", HelloWeb.DemoChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # HelloWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /lib/hello_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn (error) -> 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end) 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # When using gettext, we typically pass the strings we want 22 | # to translate as a static argument: 23 | # 24 | # # Translate "is invalid" in the "errors" domain 25 | # dgettext "errors", "is invalid" 26 | # 27 | # # Translate the number of files with plural rules 28 | # dngettext "errors", "1 file", "%{count} files", count 29 | # 30 | # Because the error messages we show in our forms and APIs 31 | # are defined inside Ecto, we need to translate them dynamically. 32 | # This requires us to call the Gettext module passing our gettext 33 | # backend as first argument. 34 | # 35 | # Note we use the "errors" domain, which means translations 36 | # should be written to the errors.po file. The :count option is 37 | # set by Ecto and indicates we should also apply plural rules. 38 | if count = opts[:count] do 39 | Gettext.dngettext(HelloWeb.Gettext, "errors", msg, msg, count, opts) 40 | else 41 | Gettext.dgettext(HelloWeb.Gettext, "errors", msg, opts) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /assets/brunch-config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // See http://brunch.io/#documentation for docs. 3 | files: { 4 | javascripts: { 5 | joinTo: "js/app.js" 6 | 7 | // To use a separate vendor.js bundle, specify two files path 8 | // http://brunch.io/docs/config#-files- 9 | // joinTo: { 10 | // "js/app.js": /^js/, 11 | // "js/vendor.js": /^(?!js)/ 12 | // } 13 | // 14 | // To change the order of concatenation of files, explicitly mention here 15 | // order: { 16 | // before: [ 17 | // "vendor/js/jquery-2.1.1.js", 18 | // "vendor/js/bootstrap.min.js" 19 | // ] 20 | // } 21 | }, 22 | stylesheets: { 23 | joinTo: "css/app.css" 24 | }, 25 | templates: { 26 | joinTo: "js/app.js" 27 | } 28 | }, 29 | 30 | conventions: { 31 | // This option sets where we should place non-css and non-js assets in. 32 | // By default, we set this to "/assets/static". Files in this directory 33 | // will be copied to `paths.public`, which is "priv/static" by default. 34 | assets: /^(static)/ 35 | }, 36 | 37 | // Phoenix paths configuration 38 | paths: { 39 | // Dependencies and current project directories to watch 40 | watched: ["static", "css", "js", "vendor"], 41 | // Where to compile files to 42 | public: "../priv/static" 43 | }, 44 | 45 | // Configure your plugins 46 | plugins: { 47 | babel: { 48 | // Do not use ES6 compiler in vendor code 49 | ignore: [/vendor/], 50 | presets: [ "es2015", "react" ] 51 | } 52 | }, 53 | 54 | modules: { 55 | autoRequire: { 56 | "js/app.js": ["js/app"] 57 | } 58 | }, 59 | 60 | npm: { 61 | enabled: true, 62 | whitelist: ["phoenix", "phoenix_html", "react", "react-dom", "redux", "react-redux"] 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /lib/hello_web.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use HelloWeb, :controller 9 | use HelloWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: HelloWeb 23 | import Plug.Conn 24 | import HelloWeb.Router.Helpers 25 | import HelloWeb.Gettext 26 | end 27 | end 28 | 29 | def view do 30 | quote do 31 | use Phoenix.View, root: "lib/hello_web/templates", 32 | namespace: HelloWeb 33 | 34 | # Import convenience functions from controllers 35 | import Phoenix.Controller, only: [get_flash: 2, view_module: 1] 36 | 37 | # Use all HTML functionality (forms, tags, etc) 38 | use Phoenix.HTML 39 | 40 | import HelloWeb.Router.Helpers 41 | import HelloWeb.ErrorHelpers 42 | import HelloWeb.Gettext 43 | end 44 | end 45 | 46 | def router do 47 | quote do 48 | use Phoenix.Router 49 | import Plug.Conn 50 | import Phoenix.Controller 51 | end 52 | end 53 | 54 | def channel do 55 | quote do 56 | use Phoenix.Channel 57 | import HelloWeb.Gettext 58 | end 59 | end 60 | 61 | @doc """ 62 | When used, dispatch to the appropriate controller/view/etc. 63 | """ 64 | defmacro __using__(which) when is_atom(which) do 65 | apply(__MODULE__, which, []) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/hello_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule HelloWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :hello 3 | 4 | socket "/socket", HelloWeb.UserSocket 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug Plug.Static, 11 | at: "/", from: :hello, gzip: false, 12 | only: ~w(css fonts images js favicon.ico robots.txt) 13 | 14 | # Code reloading can be explicitly enabled under the 15 | # :code_reloader configuration of your endpoint. 16 | if code_reloading? do 17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 18 | plug Phoenix.LiveReloader 19 | plug Phoenix.CodeReloader 20 | end 21 | 22 | plug Plug.Logger 23 | 24 | plug Plug.Parsers, 25 | parsers: [:urlencoded, :multipart, :json], 26 | pass: ["*/*"], 27 | json_decoder: Poison 28 | 29 | plug Plug.MethodOverride 30 | plug Plug.Head 31 | 32 | # The session will be stored in the cookie and signed, 33 | # this means its contents can be read but not tampered with. 34 | # Set :encryption_salt if you would also like to encrypt it. 35 | plug Plug.Session, 36 | store: :cookie, 37 | key: "_hello_key", 38 | signing_salt: "9MH8nX/2" 39 | 40 | plug HelloWeb.Router 41 | 42 | @doc """ 43 | Callback invoked for dynamically configuring the endpoint. 44 | 45 | It receives the endpoint configuration and checks if 46 | configuration should be loaded from the system environment. 47 | """ 48 | def init(_key, config) do 49 | if config[:load_from_system_env] do 50 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 51 | {:ok, Keyword.put(config, :http, [:inet6, port: port])} 52 | else 53 | {:ok, config} 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :hello, HelloWeb.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin", 15 | cd: Path.expand("../assets", __DIR__)]] 16 | 17 | config :peerage, via: Peerage.Via.Self 18 | 19 | # ## SSL Support 20 | # 21 | # In order to use HTTPS in development, a self-signed 22 | # certificate can be generated by running the following 23 | # command from your terminal: 24 | # 25 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem 26 | # 27 | # The `http:` config above can be replaced with: 28 | # 29 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"], 30 | # 31 | # If desired, both `http:` and `https:` keys can be 32 | # configured to run both http and https servers on 33 | # different ports. 34 | 35 | # Watch static and templates for browser reloading. 36 | config :hello, HelloWeb.Endpoint, 37 | live_reload: [ 38 | patterns: [ 39 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 40 | ~r{priv/gettext/.*(po)$}, 41 | ~r{lib/hello_web/views/.*(ex)$}, 42 | ~r{lib/hello_web/templates/.*(eex)$} 43 | ] 44 | ] 45 | 46 | # Do not include metadata nor timestamps in development logs 47 | config :logger, level: :info 48 | 49 | # Set a higher stacktrace during development. Avoid configuring such 50 | # in production as building large stacktraces may be expensive. 51 | config :phoenix, :stacktrace_depth, 20 52 | -------------------------------------------------------------------------------- /assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* This file is for your main application css. */ 2 | body { 3 | background: papayawhip; 4 | padding: 0; 5 | margin: 0; 6 | font-family: "SF Mono", "Droid Sans Mono", "Source Code Pro", monospace; 7 | } 8 | 9 | input { 10 | display: none; 11 | } 12 | 13 | .App { 14 | position: relative; 15 | } 16 | 17 | .Info { 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | padding: 10px; 22 | color: #fff; 23 | background: #222; 24 | opacity: 0.8; 25 | } 26 | 27 | .--cards { 28 | display: grid; 29 | grid-template-columns: repeat(5, 1fr); 30 | grid-gap: 1vw; 31 | grid-auto-rows: minmax(100px, auto); 32 | 33 | height: 50vw; 34 | width: 50vw; 35 | 36 | position: absolute; 37 | left: 0; 38 | top: 5vw; 39 | right: 0; 40 | margin: auto auto; 41 | 42 | } 43 | 44 | .Card { 45 | perspective: 1000px; 46 | transform-style: preserve-3d; 47 | cursor: pointer; 48 | } 49 | 50 | .Card .--sides { 51 | position: relative; 52 | height: 100%; 53 | width: 100%; 54 | transform-style: preserve-3d; 55 | transition: all 500ms; 56 | z-index: 20; 57 | /** figure out how to -not- display shadow at top when checked */ 58 | box-shadow: 1px 3px 3px rgba(20,20,20,.2); 59 | } 60 | 61 | .Card .--sides div { 62 | position: absolute; 63 | height: 100%; 64 | width: 100%; 65 | /* backface-visibility: hidden; */ 66 | border-radius: 2px; 67 | } 68 | 69 | .Card .--sides .--front { 70 | transform: rotateX(180deg); 71 | } 72 | 73 | .Card .--sides .--back { 74 | transform: rotateX(180deg); 75 | } 76 | 77 | .Card:hover .--sides, 78 | .Card:active .--sides, 79 | .Card:focus .--sides { 80 | transform: rotateX(20deg); 81 | box-shadow: 0 12px 12px rgba(20,20,20,.2); 82 | } 83 | 84 | :checked + .--sides { 85 | transform: rotateX(180deg); 86 | } 87 | 88 | .Card:hover :checked + .--sides, 89 | .Card:active :checked + .--sides, 90 | .Card:focus :checked + .--sides { 91 | transform: rotateX(160deg); 92 | } 93 | 94 | .Info { 95 | } 96 | -------------------------------------------------------------------------------- /rel/config.exs: -------------------------------------------------------------------------------- 1 | # Import all plugins from `rel/plugins` 2 | # They can then be used by adding `plugin MyPlugin` to 3 | # either an environment, or release definition, where 4 | # `MyPlugin` is the name of the plugin module. 5 | Path.join(["rel", "plugins", "*.exs"]) 6 | |> Path.wildcard() 7 | |> Enum.map(&Code.eval_file(&1)) 8 | 9 | use Mix.Releases.Config, 10 | # This sets the default release built by `mix release` 11 | default_release: :default, 12 | # This sets the default environment used by `mix release` 13 | default_environment: Mix.env() 14 | 15 | # For a full list of config options for both releases 16 | # and environments, visit https://hexdocs.pm/distillery/configuration.html 17 | 18 | 19 | # You may define one or more environments in this file, 20 | # an environment's settings will override those of a release 21 | # when building in that environment, this combination of release 22 | # and environment configuration is called a profile 23 | 24 | environment :dev do 25 | # If you are running Phoenix, you should make sure that 26 | # server: true is set and the code reloader is disabled, 27 | # even in dev mode. 28 | # It is recommended that you build with MIX_ENV=prod and pass 29 | # the --env flag to Distillery explicitly if you want to use 30 | # dev mode. 31 | set dev_mode: true 32 | set include_erts: false 33 | set cookie: :"=1!B03$E/mWifd4]5(Y?RG79s4;,9UI:EL=r3xiTB{T@_>kv/0F(Po&41B53/HH]" 34 | end 35 | 36 | environment :prod do 37 | set include_erts: true 38 | set include_src: false 39 | set cookie: :"E%laaP/v.xb5EWpK(:GIt3|z/Fe)&StMks4KdBARt{tcV/UqAJ_nw=vlKO;)F%l7" 40 | set vm_args: "rel/vm.args" 41 | end 42 | 43 | # You may define one or more releases in this file. 44 | # If you have not set a default release, or selected one 45 | # when running `mix release`, the first release in the file 46 | # will be used by default 47 | 48 | release :hello do 49 | set version: current_version(:hello) 50 | set applications: [ 51 | :runtime_tools 52 | ] 53 | end 54 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we often load configuration from external 4 | # sources, such as your system environment. For this reason, 5 | # you won't find the :http configuration below, but set inside 6 | # HelloWeb.Endpoint.init/2 when load_from_system_env is 7 | # true. Any dynamic configuration should be done there. 8 | # 9 | # Don't forget to configure the url host to something meaningful, 10 | # Phoenix uses this information when generating URLs. 11 | # 12 | # Finally, we also include the path to a cache manifest 13 | # containing the digested version of static files. This 14 | # manifest is generated by the mix phx.digest task 15 | # which you typically run after static files are built. 16 | config :hello, HelloWeb.Endpoint, 17 | load_from_system_env: true, 18 | http: [port: "${PORT}"], 19 | check_origin: false, 20 | server: true, 21 | root: ".", 22 | cache_static_manifest: "priv/static/cache_manifest.json" 23 | 24 | # Do not print debug messages in production 25 | config :logger, level: :info 26 | 27 | config :peerage, via: Peerage.Via.Dns, 28 | dns_name: "headless-service.hello.svc.cluster.local", 29 | app_name: "hello", 30 | interval: 1 31 | 32 | # ## SSL Support 33 | # 34 | # To get SSL working, you will need to add the `https` key 35 | # to the previous section and set your `:url` port to 443: 36 | # 37 | # config :hello, HelloWeb.Endpoint, 38 | # ... 39 | # url: [host: "example.com", port: 443], 40 | # https: [:inet6, 41 | # port: 443, 42 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 43 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 44 | # 45 | # Where those two env variables return an absolute path to 46 | # the key and cert in disk or a relative path inside priv, 47 | # for example "priv/ssl/server.key". 48 | # 49 | # We also recommend setting `force_ssl`, ensuring no data is 50 | # ever sent via http, always redirecting to https: 51 | # 52 | # config :hello, HelloWeb.Endpoint, 53 | # force_ssl: [hsts: true] 54 | # 55 | # Check `Plug.SSL` for all available options in `force_ssl`. 56 | 57 | # ## Using releases 58 | # 59 | # If you are doing OTP releases, you need to instruct Phoenix 60 | # to start the server for all endpoints: 61 | # 62 | # config :phoenix, :serve_endpoints, true 63 | # 64 | # Alternatively, you can configure exactly which server to 65 | # start per endpoint: 66 | # 67 | # config :hello, HelloWeb.Endpoint, server: true 68 | # 69 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import "phoenix_html" 3 | import React from "react" 4 | import ReactDOM from "react-dom" 5 | import {Socket} from "phoenix" 6 | 7 | alert('hello') 8 | 9 | const COLORS = [ 10 | 'c4e899', 11 | '95d7a4', 12 | '69b18f', 13 | '307e80', 14 | '034262', 15 | 'ff7575', 16 | 'ff8775', 17 | 'ffba79', 18 | 'ffcb71', 19 | '292e32', 20 | 'cccccc', 21 | '7aa0c4', 22 | '34568f', 23 | '2f425e', 24 | 'f0f7ce', 25 | 'bbeaaa', 26 | '9fdda5', 27 | 'f59ba5', 28 | 'f87d92', 29 | ] 30 | 31 | const randColor = () => COLORS[Math.floor(Math.random()*COLORS.length)] 32 | const randIndex = () => Math.floor(Math.random() * 25) 33 | 34 | type Props = { 35 | } 36 | 37 | type State = { 38 | image: Array 39 | } 40 | 41 | class App extends React.Component { 42 | constructor(props) { 43 | super(props) 44 | 45 | this.color = randColor() 46 | document.querySelector('body').style.background = '#' + this.color + 'ee' 47 | 48 | this.cardRefs = [] 49 | 50 | this.state = { 51 | cards: [] 52 | } 53 | 54 | } 55 | 56 | componentDidMount() { 57 | const socket = new Socket("/socket", {params: {}}) 58 | socket.connect() 59 | this.channel = socket.channel("demo:hello", {}) 60 | 61 | this.channel.join() 62 | .receive("ok", (res) => { 63 | console.log('joined!', res) 64 | }) 65 | .receive("error", (res) => { 66 | console.log('error joining', res) 67 | }) 68 | 69 | this.channel.on("cards:state", ({ data }) => { 70 | this.setState({ cards: data }) 71 | }) 72 | 73 | this.channel.on("cards:change", ({ index, color }) => { 74 | const cards = this.state.cards 75 | cards[index] = color 76 | this.setState({ cards }) 77 | 78 | if (this.color !== color) { 79 | this.cardRefs[index].checked = !this.cardRefs[index].checked 80 | } 81 | }) 82 | } 83 | 84 | handleClick(e, index) { 85 | if (e.target.type === 'checkbox') { 86 | this.channel.push("cards:change", { index, color: this.color }) 87 | } 88 | } 89 | 90 | render() { 91 | const cards = this.state.cards.map((color, i) => { 92 | return ( 93 | this.cardRefs[i] = c } 97 | onClick={(e) => this.handleClick(e, i)} /> 98 | ) 99 | }) 100 | 101 | return ( 102 |
103 |
104 | { cards } 105 |
106 |
107 | ) 108 | } 109 | } 110 | 111 | const Card = (props: { reffn: *, colors: [string, string], onClick: * }) => { 112 | return ( 113 |