diff --git a/.controlplane/Dockerfile b/.controlplane/Dockerfile new file mode 100644 index 00000000..8a809700 --- /dev/null +++ b/.controlplane/Dockerfile @@ -0,0 +1,82 @@ +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile +ARG RUBY_VERSION=3.3.4 +FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base + +# Current commit hash environment variable +ARG GIT_COMMIT +ENV GIT_COMMIT_SHA=${GIT_COMMIT} + +# Install packages needed to build gems and node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential curl git libpq-dev libvips node-gyp pkg-config python-is-python3 + +# Install JavaScript dependencies +# Make sure NODE_VERSION matches the node version in .nvmrc and package.json +ARG NODE_VERSION=22.3.0 +ARG YARN_VERSION=1.22.19 +ENV PATH=/usr/local/node/bin:$PATH +RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \ + /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \ + npm install -g yarn@$YARN_VERSION && \ + rm -rf /tmp/node-build-master + +# Rails app lives here +# Entry point and commands will be run from this directory +WORKDIR /app + +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development test" + + +# Throw-away build stage to reduce size of final image +FROM base as build + +# Install application gems +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git + +# Install node modules +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile + +# Copy application code +COPY . . + +# Final stage for app image +FROM base + +# Install packages needed for deployment +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libvips postgresql-client && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Copy built artifacts: gems, application +COPY --from=build /usr/local/bundle /usr/local/bundle +COPY --from=build /app /app + +RUN chmod +x /app/.controlplane/*.sh + +ENV RAILS_ENV=production \ + NODE_ENV=production \ + SECRET_KEY_BASE=NOT_USED_NON_BLANK +# compiling assets requires any value for ENV of SECRET_KEY_BASE + +# These files hardly ever change +RUN bin/rails react_on_rails:locale + +# These files change together, /app/lib/bs are temp build files for rescript, +# and /app/client/app are the client assets that are bundled, so not needed once built +# Helps to have smaller images b/c of smaller Docker Layer Caches and smaller final images +RUN yarn res:build && bin/rails assets:precompile && rm -rf /app/lib/bs /app/client/app + +# This is like the shell initialization that will take the CMD as args +# For Kubernetes and ControlPlane, this is the command on the workload. +ENTRYPOINT ["./.controlplane/entrypoint.sh"] + +# Default args to pass to the entry point that can be overridden +# For Kubernetes and ControlPlane, these are the "workload args" +CMD ["./bin/rails", "server"] diff --git a/.controlplane/controlplane.yml b/.controlplane/controlplane.yml new file mode 100644 index 00000000..74dc1583 --- /dev/null +++ b/.controlplane/controlplane.yml @@ -0,0 +1,75 @@ +# Configuration for `cpflow` commands. + +# Keys beginning with "cpln_" correspond to your settings in Control Plane. + +# Global settings that apply to `cpflow` usage. +# You can opt out of allowing the use of CPLN_ORG and CPLN_APP env vars +# to avoid any accidents with the wrong org / app. +allow_org_override_by_env: true +allow_app_override_by_env: true + +aliases: + common: &common + # Org for staging and QA apps is typically set as an alias, shared by all apps, except for production apps. + # Production apps will use a different org than staging for security. + # Change this value to your org name + # or set ENV CPLN_ORG to your org name as that will override whatever is used here for all cpflow commands + cpln_org: shakacode-open-source-examples-staging + + # Example apps use only location. CPLN offers the ability to use multiple locations. + default_location: aws-us-east-2 + # Configure the workload name used as a template for one-off scripts, like a Heroku one-off dyno. + one_off_workload: rails + # Like the entries in the Heroku Procfile that get deployed when the application code changes + # and the application image updates. + app_workloads: + - rails + - daily-task + # Additional workloads that are not affected by deploy-image and promote-app-from-upstream + # These workloads apply to the ps commands + additional_workloads: + - redis + - postgres + + # Configure the workload name used when maintenance mode is on (defaults to "maintenance"). + maintenance_workload: maintenance + + # Configure the script to run when releasing an app., either with deploy-image or promote-app-from-upstream + release_script: release_script.sh + +apps: + react-webpack-rails-tutorial-production: + # Simulate Production Version + <<: *common + # Don't allow overriding the org and app by ENV vars b/c production is sensitive! + allow_org_override_by_env: false + allow_app_override_by_env: false + + # Use a different organization for production. + cpln_org: shakacode-open-source-examples-production + + upstream: react-webpack-rails-tutorial-staging + + react-webpack-rails-tutorial-staging: + <<: *common + # QA Apps are like Heroku review apps, but the use `prefix` so you can run a commmand like + # this to create a QA app for the tutorial app. + # `cpflow setup gvc postgres redis rails -a qa-react-webpack-rails-tutorial-pr-1234` + qa-react-webpack-rails-tutorial: + <<: *common + # Order matters! + setup_app_templates: + # GVC template contains the identity + - app + + # Resources + - postgres + - redis + + # Workloads, like Dynos types on Heroku + - daily-task + - rails + # match_if_app_name_starts_with is used to identify these "qa" apps. + match_if_app_name_starts_with: true + image_retention_days: 5 + stale_app_image_deployed_days: 5 # If the app is older than 5 days, the nightly automations will clean stale apps. diff --git a/.controlplane/entrypoint.sh b/.controlplane/entrypoint.sh new file mode 100755 index 00000000..d80de4c3 --- /dev/null +++ b/.controlplane/entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/bash -e +# Runs before the main command +# This script is unique to this demo project as it ensures the database and Redis are ready +# before running the rails server or other services. +# You can ignore this sort of "wait" if using external services, like AWS RDS or AWS Aurora. + +wait_for_service() +{ + until curl -I -sS $1 2>&1 | grep -q "Empty reply from server"; do + echo " -- $1 is unavailable, sleeping..." + sleep 1 + done + echo " -- $1 is available" +} + +echo " -- Starting entrypoint.sh" + +echo " -- Waiting for services" + +# Strip out the host and the port for curl and to keep full resource URL secret +wait_for_service $(echo $DATABASE_URL | sed -e 's|^.*@||' -e 's|/.*$||') +wait_for_service $(echo $REDIS_URL | sed -e 's|redis://||' -e 's|/.*$||') + +echo " -- Finishing entrypoint.sh, executing '$@'" + +# Run the main command +exec "$@" diff --git a/.controlplane/readme.md b/.controlplane/readme.md new file mode 100644 index 00000000..d3fe1850 --- /dev/null +++ b/.controlplane/readme.md @@ -0,0 +1,154 @@ +# Deploying tutorial app on Control Plane + +--- + +_If you need a free demo account for Control Plane (no CC required), you can contact [Justin Gordon, CEO of ShakaCode](mailto:justin@shakacode.com)._ + +--- + +Check [how the `cpflow` gem (this project) is used in the Github actions](https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/.github/actions/deploy-to-control-plane/action.yml). +Here is a brief [video overview](https://www.youtube.com/watch?v=llaQoAV_6Iw). + +--- + +## Overview +This simple example shows how to deploy a simple app on Control Plane using the `cpflow` gem. + +To maximize simplicity, this example creates Postgres and Redis as workloads in the same GVC as the app. +In a real app, you would likely use persistent, external resources, such as AWS RDS and AWS ElastiCache. + +You can see the definition of Postgres and Redis in the `.controlplane/templates` directory. + +## Prerequisites + +1. Ensure your [Control Plane](https://shakacode.controlplane.com) account is set up. +You should have an `organization` `` for testing in that account. +Set ENV variable `CPLN_ORG` to ``. Alternatively, you may modify the +value for `aliases.common.cpln_org` in `.controlplane/controlplane.yml`. +If you need an organization, please [contact Shakacode](mailto:controlplane@shakacode.com). + +2. Install Control Plane CLI (and configure access) using `npm install -g @controlplane/cli`. +You can update the `cpln` command line with `npm update -g @controlplane/cli`. +Then run `cpln login` to ensure access. +For more informatation check out the +[docs here](https://shakadocs.controlplane.com/quickstart/quick-start-3-cli#getting-started-with-the-cli). + +3. Run `cpln image docker-login --org ` to ensure that you have access to the Control Plane Docker registry. + +4. Install the latest version of +[`cpflow` gem](https://rubygems.org/gems/cpflow) +on your project's Gemfile or globally. +For more information check out +[Heroku to Control Plane](https://github.com/shakacode/heroku-to-control-plane). + +5. This project has a `Dockerfile` for Control Plane in `.controlplane` directory. +You can use it as an example for your project. +Ensure that you have Docker running. + +### Tips +Do not confuse the `cpflow` CLI with the `cpln` CLI. +The `cpflow` CLI is the Heroku to Control Plane playbook CLI. +The `cpln` CLI is the Control Plane CLI. + +## Project Configuration +See the filese in the `./controlplane` directory. + +1. `/templates`: defines the objects created with the `cpflow setup` command. +These YAML files are the same as used by the `cpln apply` command. +2. `/controlplane.yml`: defines your application, including the organization, location, and app name. +3. `Dockerfile`: defines the Docker image used to run the app on Control Plane. +4. `entrypoint.sh`: defines the entrypoint script used to run the app on Control Plane. + +## Setup and run + +Check if the Control Plane organization and location are correct in `.controlplane/controlplane.yml`. +Alternatively, you can use `CPLN_ORG` environment variable to set the organization name. +You should be able to see this information in the Control Plane UI. + +**Note:** The below commands use `cpflow` which is the Heroku to Control Plane playbook gem, +and not `cpln` which is the Control Plane CLI. + +```sh +# Use environment variable to prevent repetition +export APP_NAME=react-webpack-rails-tutorial + +# Provision all infrastructure on Control Plane. +# app react-webpack-rails-tutorial will be created per definition in .controlplane/controlplane.yml +cpflow setup-app -a $APP_NAME + +# Build and push docker image to Control Plane repository +# Note, may take many minutes. Be patient. +# Check for error messages, such as forgetting to run `cpln image docker-login --org ` +cpflow build-image -a $APP_NAME + +# Promote image to app after running `cpflow build-image command` +# Note, the UX of images may not show the image for up to 5 minutes. +# However, it's ready. +cpflow deploy-image -a $APP_NAME + +# See how app is starting up +cpflow logs -a $APP_NAME + +# Open app in browser (once it has started up) +cpflow open -a $APP_NAME +``` + +### Promoting code updates + +After committing code, you will update your deployment of `react-webpack-rails-tutorial` with the following commands: + +```sh +# Assuming you have already set APP_NAME env variable to react-webpack-rails-tutorial +# Build and push new image with sequential image tagging, e.g. 'react-webpack-rails-tutorial:1', then 'react-webpack-rails-tutorial:2', etc. +cpflow build-image -a $APP_NAME + +# Run database migrations (or other release tasks) with latest image, +# while app is still running on previous image. +# This is analogous to the release phase. +cpflow run -a $APP_NAME --image latest -- rails db:migrate + +# Pomote latest image to app after migrations run +cpflow deploy-image -a $APP_NAME +``` + +If you needed to push a new image with a specific commit SHA, you can run the following command: + +```sh +# Build and push with sequential image tagging and commit SHA, e.g. 'react-webpack-rails-tutorial:123_ABCD' +cpflow build-image -a $APP_NAME --commit ABCD +``` + +## Other notes + +### `entrypoint.sh` +- waits for Postgres and Redis to be available +- runs `rails db:prepare` to create/seed or migrate the database + +## CI Automation, Review Apps and Staging + +_Note, some of the URL references are internal for the ShakaCode team._ + + Review Apps (deployment of apps based on a PR) are done via Github Actions. + +The review apps work by creating isolated deployments for each branch through this automated process. When a branch is pushed, the action: + +1. Sets up the necessary environment and tools +2. Creates a unique deployment for that branch if it doesn't exist +3. Builds a Docker image tagged with the branch's commit SHA +4. Deploys this image to Control Plane with its own isolated environment + +This allows teams to: +- Preview changes in a production-like environment +- Test features independently +- Share working versions with stakeholders +- Validate changes before merging to main branches + +The system uses Control Plane's infrastructure to manage these deployments, with each branch getting its own resources as defined in the controlplane.yml configuration. + + +### Workflow for Developing Github Actions for Review Apps + +1. Create a PR with changes to the Github Actions workflow +2. Make edits to file such as `.github/actions/deploy-to-control-plane/action.yml` +3. Run a script like `ga .github && gc -m fixes && gp` to commit and push changes (ga = git add, gc = git commit, gp = git push) +4. Check the Github Actions tab in the PR to see the status of the workflow diff --git a/.controlplane/release_script.sh b/.controlplane/release_script.sh new file mode 100755 index 00000000..fe2ab785 --- /dev/null +++ b/.controlplane/release_script.sh @@ -0,0 +1,22 @@ +#!/bin/bash -e + + +log() { + echo "[`date +%Y-%m-%d:%H:%M:%S`]: $1" +} + +error_exit() { + log "$1" 1>&2 + exit 1 +} + +log 'Running release_script.sh per controlplane.yml' + +if [ -x ./bin/rails ]; then + log 'Run DB migrations' + ./bin/rails db:prepare || error_exit "Failed to run DB migrations" +else + error_exit "./bin/rails does not exist or is not executable" +fi + +log 'Completed release_script.sh per controlplane.yml' diff --git a/.controlplane/shakacode-team.md b/.controlplane/shakacode-team.md new file mode 100644 index 00000000..0a6273a0 --- /dev/null +++ b/.controlplane/shakacode-team.md @@ -0,0 +1,6 @@ +# Internal Notes to the Shakacode Team + +## Links + +- [Control Plane Org for Staging and Review Apps](https://console.cpln.io/console/org/shakacode-open-source-examples-staging/-info) +- [Control Plane Org for Deployed App](https://console.cpln.io/console/org/shakacode-open-source-examples/-info) diff --git a/.controlplane/templates/app.yml b/.controlplane/templates/app.yml new file mode 100644 index 00000000..09249b7d --- /dev/null +++ b/.controlplane/templates/app.yml @@ -0,0 +1,30 @@ +# Template setup of the GVC, roughly corresponding to a Heroku app +kind: gvc +name: {{APP_NAME}} +spec: + # For using templates for test apps, put ENV values here, stored in git repo. + # Production apps will have values configured manually after app creation. + env: + - name: DATABASE_URL + # Password does not matter because host postgres.{{APP_NAME}}.cpln.local can only be accessed + # locally within CPLN GVC, and postgres running on a CPLN workload is something only for a + # test app that lacks persistence. + value: 'postgres://the_user:the_password@postgres.{{APP_NAME}}.cpln.local:5432/{{APP_NAME}}' + - name: RAILS_ENV + value: production + - name: NODE_ENV + value: production + - name: RAILS_SERVE_STATIC_FILES + value: 'true' + - name: REDIS_URL + # No password for GVC local Redis. See comment above for postgres. + value: 'redis://redis.{{APP_NAME}}.cpln.local:6379' + # Part of standard configuration + staticPlacement: + locationLinks: + - {{APP_LOCATION_LINK}} + +--- +# Identity is needed to access secrets +kind: identity +name: {{APP_IDENTITY}} diff --git a/.controlplane/templates/daily-task.yml b/.controlplane/templates/daily-task.yml new file mode 100644 index 00000000..f6b8dd9f --- /dev/null +++ b/.controlplane/templates/daily-task.yml @@ -0,0 +1,34 @@ +kind: workload +name: daily-task +spec: + # https://docs.controlplane.com/reference/workload#cron-configuration + type: cron + job: + activeDeadlineSeconds: 3600 + concurrencyPolicy: Forbid + historyLimit: 5 + restartPolicy: Never + # daily. See cron docs + schedule: 0 0 * * * + containers: + - name: daily-task + cpu: 50m + memory: 256Mi + args: + - bundle + - exec + - rake + - daily + inheritEnv: true + image: {{APP_IMAGE_LINK}} + defaultOptions: + autoscaling: + minScale: 1 + maxScale: 1 + capacityAI: false + firewallConfig: + external: + outboundAllowCIDR: + - 0.0.0.0/0 + # Identity is used for binding workload to secrets + identityLink: {{APP_IDENTITY_LINK}} diff --git a/.controlplane/templates/maintenance.yml b/.controlplane/templates/maintenance.yml new file mode 100644 index 00000000..2e3ad59f --- /dev/null +++ b/.controlplane/templates/maintenance.yml @@ -0,0 +1,25 @@ +kind: workload +name: maintenance +spec: + type: standard + containers: + - name: maintenance + env: + - name: PORT + value: "3000" + - name: PAGE_URL + value: "" + image: "shakacode/maintenance-mode" + ports: + - number: 3000 + protocol: http + defaultOptions: + autoscaling: + minScale: 1 + maxScale: 1 + capacityAI: false + timeoutSeconds: 60 + firewallConfig: + external: + inboundAllowCIDR: + - 0.0.0.0/0 diff --git a/.controlplane/templates/org.yml b/.controlplane/templates/org.yml new file mode 100644 index 00000000..6616376d --- /dev/null +++ b/.controlplane/templates/org.yml @@ -0,0 +1,23 @@ +# Org level secrets are used to store sensitive information that is +# shared across multiple apps in the same organization. This is +# useful for storing things like API keys, database credentials, and +# other sensitive information that is shared across multiple apps +# in the same organization. + +# This is how you apply this once (not during CI) +# cpl apply-template secrets -a qa-react-webpack-rails-tutorial --org shakacode-open-source-examples-staging + +kind: secret +name: {{APP_SECRETS}} +type: dictionary +data: + SOME_ENV: "123456" + +--- + +# Policy is needed to allow identities to access secrets +kind: policy +name: {{APP_SECRETS_POLICY}} +targetKind: secret +targetLinks: + - //secret/{{APP_SECRETS}} diff --git a/.controlplane/templates/postgres.yml b/.controlplane/templates/postgres.yml new file mode 100644 index 00000000..77e3497b --- /dev/null +++ b/.controlplane/templates/postgres.yml @@ -0,0 +1,173 @@ +# Comes from example at +# https://github.com/controlplane-com/examples/blob/main/examples/postgres/manifest.yaml + +kind: volumeset +name: postgres-poc-vs +description: postgres-poc-vs +spec: + autoscaling: + maxCapacity: 1000 + minFreePercentage: 1 + scalingFactor: 1.1 + fileSystemType: ext4 + initialCapacity: 10 + performanceClass: general-purpose-ssd + snapshots: + createFinalSnapshot: true + retentionDuration: 7d + +--- +kind: secret +name: postgres-poc-credentials +description: '' +type: dictionary +data: + password: the_password #Replace this with a real password + username: the_user #Replace this with a real username + +--- +kind: secret +name: postgres-poc-entrypoint-script +type: opaque +data: + encoding: base64 + payload: >- + IyEvdXNyL2Jpbi9lbnYgYmFzaAoKc291cmNlIC91c3IvbG9jYWwvYmluL2RvY2tlci1lbnRyeXBvaW50LnNoCgppbnN0YWxsX2RlcHMoKSB7CiAgYXB0LWdldCB1cGRhdGUgLXkgPiAvZGV2L251bGwKICBhcHQtZ2V0IGluc3RhbGwgY3VybCAteSA+IC9kZXYvbnVsbAogIGFwdC1nZXQgaW5zdGFsbCB1bnppcCAteSA+IC9kZXYvbnVsbAogIGN1cmwgImh0dHBzOi8vYXdzY2xpLmFtYXpvbmF3cy5jb20vYXdzY2xpLWV4ZS1saW51eC14ODZfNjQuemlwIiAtbyAiYXdzY2xpdjIuemlwIiA+IC9kZXYvbnVsbAogIHVuemlwIGF3c2NsaXYyLnppcCA+IC9kZXYvbnVsbAogIC4vYXdzL2luc3RhbGwgPiAvZGV2L251bGwKfQoKZGJfaGFzX2JlZW5fcmVzdG9yZWQoKSB7CiAgaWYgWyAhIC1mICIkUEdEQVRBL0NQTE5fUkVTVE9SRUQiIF07IHRoZW4KICAgIHJldHVybiAxCiAgZmkKCiAgaWYgISBncmVwIC1xICJcLT4gJDEkIiAiJFBHREFUQS9DUExOX1JFU1RPUkVEIjsgdGhlbgogICAgcmV0dXJuIDEKICBlbHNlCiAgICByZXR1cm4gMAogIGZpCn0KCnJlc3RvcmVfZGIoKSB7Cgl3aGlsZSBbICEgLVMgL3Zhci9ydW4vcG9zdGdyZXNxbC8ucy5QR1NRTC41NDMyIF0KCWRvCiAgICBlY2hvICJXYWl0aW5nIDVzIGZvciBkYiBzb2NrZXQgdG8gYmUgYXZhaWxhYmxlIgogICAgc2xlZXAgNXMKICBkb25lCgoKCWlmICEgZGJfaGFzX2JlZW5fcmVzdG9yZWQgIiQxIjsgdGhlbgoJICBlY2hvICJJdCBhcHBlYXJzIGRiICckMScgaGFzIG5vdCB5ZXQgYmVlbiByZXN0b3JlZCBmcm9tIFMzLiBBdHRlbXB0aW5nIHRvIHJlc3RvcmUgJDEgZnJvbSAkMiIKCSAgaW5zdGFsbF9kZXBzCgkgIGRvY2tlcl9zZXR1cF9kYiAjRW5zdXJlcyAkUE9TVEdSRVNfREIgZXhpc3RzIChkZWZpbmVkIGluIHRoZSBlbnRyeXBvaW50IHNjcmlwdCBmcm9tIHRoZSBwb3N0Z3JlcyBkb2NrZXIgaW1hZ2UpCgkgIGF3cyBzMyBjcCAiJDIiIC0gfCBwZ19yZXN0b3JlIC0tY2xlYW4gLS1uby1hY2wgLS1uby1vd25lciAtZCAiJDEiIC1VICIkUE9TVEdSRVNfVVNFUiIKCSAgZWNobyAiJChkYXRlKTogJDIgLT4gJDEiIHwgY2F0ID4+ICIkUEdEQVRBL0NQTE5fUkVTVE9SRUQiCgllbHNlCgkgIGVjaG8gIkRiICckMScgYWxyZWFkeSBleGlzdHMuIFJlYWR5ISIKICBmaQp9CgpfbWFpbiAiJEAiICYKYmFja2dyb3VuZFByb2Nlc3M9JCEKCmlmIFsgLW4gIiRQT1NUR1JFU19BUkNISVZFX1VSSSIgXTsgdGhlbgogIHJlc3RvcmVfZGIgIiRQT1NUR1JFU19EQiIgIiRQT1NUR1JFU19BUkNISVZFX1VSSSIKZWxzZQogIGVjaG8gIkRlY2xpbmluZyB0byByZXN0b3JlIHRoZSBkYiBiZWNhdXNlIG5vIGFyY2hpdmUgdXJpIHdhcyBwcm92aWRlZCIKZmkKCndhaXQgJGJhY2tncm91bmRQcm9jZXNzCgoK + +#Here is the ASCII-encoded version of the script in the secret above +#!/usr/bin/env bash +# +#source /usr/local/bin/docker-entrypoint.sh +# +#install_deps() { +# apt-get update -y > /dev/null +# apt-get install curl -y > /dev/null +# apt-get install unzip -y > /dev/null +# curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" > /dev/null +# unzip awscliv2.zip > /dev/null +# ./aws/install > /dev/null +#} +# +#db_has_been_restored() { +# if [ ! -f "$PGDATA/CPLN_RESTORED" ]; then +# return 1 +# fi +# +# if ! grep -q "\-> $1$" "$PGDATA/CPLN_RESTORED"; then +# return 1 +# else +# return 0 +# fi +#} +# +#restore_db() { +# while [ ! -S /var/run/postgresql/.s.PGSQL.5432 ] +# do +# echo "Waiting 5s for db socket to be available" +# sleep 5s +# done +# +# +# if ! db_has_been_restored "$1"; then +# echo "It appears db '$1' has not yet been restored from S3. Attempting to restore $1 from $2" +# install_deps +# docker_setup_db #Ensures $POSTGRES_DB exists (defined in the entrypoint script from the postgres docker image) +# aws s3 cp "$2" - | pg_restore --clean --no-acl --no-owner -d "$1" -U "$POSTGRES_USER" +# echo "$(date): $2 -> $1" | cat >> "$PGDATA/CPLN_RESTORED" +# else +# echo "Db '$1' already exists. Ready!" +# fi +#} +# +#_main "$@" & +#backgroundProcess=$! +# +#if [ -n "$POSTGRES_ARCHIVE_URI" ]; then +# restore_db "$POSTGRES_DB" "$POSTGRES_ARCHIVE_URI" +#else +# echo "Declining to restore the db because no archive uri was provided" +#fi +# +#wait $backgroundProcess + +--- +kind: identity +name: postgres-poc-identity +description: postgres-poc-identity + +--- +kind: policy +name: postgres-poc-access +description: postgres-poc-access +bindings: + - permissions: + - reveal +# Uncomment these two +# - use +# - view + principalLinks: + - //gvc/{{APP_NAME}}/identity/postgres-poc-identity +targetKind: secret +targetLinks: + - //secret/postgres-poc-credentials + - //secret/postgres-poc-entrypoint-script + +--- +kind: workload +name: postgres +description: postgres +spec: + type: stateful + containers: + - cpu: 1000m + memory: 512Mi + env: + # Uncomment next two envs will cause the db to be restored from the archive uri + # - name: POSTGRES_ARCHIVE_URI #Use this var to control the automatic restore behavior. If you leave it out, the db will start empty. + # value: s3://YOUR_BUCKET/PATH_TO_ARCHIVE_FILE + # - name: POSTGRES_DB #The name of the initial db in case of doing a restore + # value: test + - name: PGDATA #The location postgres stores the db. This can be anything other than /var/lib/postgresql/data, but it must be inside the mount point for the volume set + value: "/var/lib/postgresql/data/pg_data" + - name: POSTGRES_PASSWORD #The password for the default user + value: cpln://secret/postgres-poc-credentials.password + - name: POSTGRES_USER #The name of the default user + value: cpln://secret/postgres-poc-credentials.username + name: stateful + image: postgres:15 + command: /bin/bash + args: + - "-c" + - "cat /usr/local/bin/cpln-entrypoint.sh >> ./cpln-entrypoint.sh && chmod u+x ./cpln-entrypoint.sh && ./cpln-entrypoint.sh postgres" + ports: + - number: 5432 + protocol: tcp + volumes: + - uri: cpln://volumeset/postgres-poc-vs + path: "/var/lib/postgresql/data" + # Make the ENV value for the entry script a file + - uri: cpln://secret/postgres-poc-entrypoint-script + path: "/usr/local/bin/cpln-entrypoint.sh" + inheritEnv: false + livenessProbe: + tcpSocket: + port: 5432 + failureThreshold: 1 + readinessProbe: + tcpSocket: + port: 5432 + failureThreshold: 1 + identityLink: //identity/postgres-poc-identity + defaultOptions: + capacityAI: false + autoscaling: + metric: cpu + target: 95 + maxScale: 1 + firewallConfig: + external: + inboundAllowCIDR: [] + outboundAllowCIDR: + - 0.0.0.0/0 + internal: + inboundAllowType: same-gvc diff --git a/.controlplane/templates/rails.yml b/.controlplane/templates/rails.yml new file mode 100644 index 00000000..9641165b --- /dev/null +++ b/.controlplane/templates/rails.yml @@ -0,0 +1,38 @@ +# Template setup of Rails server workload, roughly corresponding to Heroku dyno +# type within Procfile. +kind: workload +name: rails +spec: + type: standard + containers: + - name: rails + # 300m is a good starting place for a test app. You can experiment with CPU configuration + # once your app is running. + cpu: 300m + env: + - name: LOG_LEVEL + value: debug + # Inherit other ENV values from GVC + inheritEnv: true + image: {{APP_IMAGE_LINK}} + # 512 corresponds to a standard 1x dyno type + memory: 512Mi + ports: + - number: 3000 + protocol: http + defaultOptions: + # Start out like this for "test apps" + autoscaling: + # Max of 1 effectively disables autoscaling, so a like a Heroku dyno count of 1 + maxScale: 1 + capacityAI: false + firewallConfig: + external: + # Default to allow public access to Rails server + inboundAllowCIDR: + - 0.0.0.0/0 + # Could configure outbound for more security + outboundAllowCIDR: + - 0.0.0.0/0 + # Identity is used for binding workload to secrets + identityLink: {{APP_IDENTITY_LINK}} diff --git a/.controlplane/templates/redis.yml b/.controlplane/templates/redis.yml new file mode 100644 index 00000000..124e665a --- /dev/null +++ b/.controlplane/templates/redis.yml @@ -0,0 +1,18 @@ +kind: workload +name: redis +spec: + type: standard + containers: + - name: redis + image: 'redis:6.2-alpine' + ports: + - number: 6379 + protocol: tcp + defaultOptions: + autoscaling: + maxScale: 1 + capacityAI: false + # This firewall configuration corresponds to using no password for Redis in the gvc.yml template. + firewallConfig: + internal: + inboundAllowType: same-gvc diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..4ddbcbc5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,52 @@ +# From .gitignore + +# Ignore bundler config. +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-journal + +# Ignore all logfiles and tempfiles. +/log/*.log +/tmp +/public/assets +.env +node_modules +npm-debug.log* +/coverage +dump.rdb +.DS_Store + +# Ignore bundle dependencies +vendor/bundle + +# Ignore GitHub Actions and workflows +.github/ + +# RVM gemset +.ruby-gemset + +# Generated js bundles +/public/packs +/public/packs-test + +# Rubymine/IntelliJ +.idea + +# Redis generated file +dump.rdb + +# Ignore i18n-js +client/app/libs/i18n/translations.js +client/app/libs/i18n/default.js + +/yarn-error.log +yarn-debug.log* +.yarn-integrity + +################################################### +# Specific to .dockerignore +.git/ +spec/ +scripts/ diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..726bf604 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +node_modules/ +tmp/ +public/ +client/app/libs/i18n/translations.js +client/app/libs/i18n/default.js +postcss.config.js +client/app/bundles/comments/rescript/ diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..2233d589 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,47 @@ +extends: + - eslint-config-shakacode + - plugin:react/recommended + - plugin:prettier/recommended + - plugin:jsx-a11y/recommended + - prettier + +plugins: + - react + - jsx-a11y + - jest + - prettier + +globals: + __DEBUG_SERVER_ERRORS__: true + __SERVER_ERRORS__: true + +env: + browser: true + node: true + jest: true + +rules: + no-shadow: 0 + no-console: 0 + function-paren-newline: 0 + object-curly-newline: 0 + no-restricted-syntax: ["error", "SequenceExpression"] + # https://stackoverflow.com/a/59268871/5241481 + import/extensions: 0 + + # https://github.com/benmosher/eslint-plugin-import/issues/340 + import/no-extraneous-dependencies: 0 + + prettier/prettier: "error" + + # currently deprecated https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-onchange.md + jsx-a11y/no-onchange: 0 + +settings: + import/core-modules: + - react-redux + import/resolver: + node: + extensions: [".js"] + webpack: + config: 'config/webpack/development.js' diff --git a/.github/actions/build-docker-image/action.yml b/.github/actions/build-docker-image/action.yml new file mode 100644 index 00000000..45a12434 --- /dev/null +++ b/.github/actions/build-docker-image/action.yml @@ -0,0 +1,39 @@ +name: Build Docker Image +description: 'Builds a Docker image for the application' + +inputs: + app_name: + description: 'Name of the application' + required: true + org: + description: 'Organization name' + required: true + commit: + description: 'Commit SHA to tag the image with' + required: true + PR_NUMBER: + description: 'PR number' + required: false + +runs: + using: "composite" + steps: + - name: Build Docker Image + id: build + shell: bash + run: | + PR_INFO="" + if [ -n "${PR_NUMBER}" ]; then + PR_INFO=" for PR #${PR_NUMBER}" + fi + + echo "๐Ÿ—๏ธ Building Docker image${PR_INFO} (commit ${{ inputs.commit }})..." + + if cpflow build-image -a "${{ inputs.app_name }}" --commit="${{ inputs.commit }}" --org="${{ inputs.org }}"; then + image_tag="${{ inputs.org }}/${{ inputs.app_name }}:${{ inputs.commit }}" + echo "image_tag=${image_tag}" >> $GITHUB_OUTPUT + echo "โœ… Docker image build successful${PR_INFO} (commit ${{ inputs.commit }})" + else + echo "โŒ Docker image build failed${PR_INFO} (commit ${{ inputs.commit }})" + exit 1 + fi diff --git a/.github/actions/delete-control-plane-app/action.yml b/.github/actions/delete-control-plane-app/action.yml new file mode 100644 index 00000000..caaef272 --- /dev/null +++ b/.github/actions/delete-control-plane-app/action.yml @@ -0,0 +1,20 @@ +name: Delete Control Plane App +description: 'Deletes a Control Plane application and all its resources' + +inputs: + app_name: + description: 'Name of the application to delete' + required: true + cpln_org: + description: 'Organization name' + required: true + +runs: + using: "composite" + steps: + - name: Delete Application + shell: bash + run: ${{ github.action_path }}/delete-app.sh + env: + APP_NAME: ${{ inputs.app_name }} + CPLN_ORG: ${{ inputs.cpln_org }} diff --git a/.github/actions/delete-control-plane-app/delete-app.sh b/.github/actions/delete-control-plane-app/delete-app.sh new file mode 100755 index 00000000..6bc92bfc --- /dev/null +++ b/.github/actions/delete-control-plane-app/delete-app.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Script to delete a Control Plane application +# Required environment variables: +# - APP_NAME: Name of the application to delete +# - CPLN_ORG: Organization name + +set -e + +# Validate required environment variables +: "${APP_NAME:?APP_NAME environment variable is required}" +: "${CPLN_ORG:?CPLN_ORG environment variable is required}" + +# Safety check: prevent deletion of production or staging apps +if echo "$APP_NAME" | grep -iqE '(production|staging)'; then + echo "โŒ ERROR: Cannot delete apps containing 'production' or 'staging' in their name" >&2 + echo "๐Ÿ›‘ This is a safety measure to prevent accidental deletion of production or staging environments" >&2 + echo " App name: $APP_NAME" >&2 + exit 1 +fi + +# Check if app exists before attempting to delete +echo "๐Ÿ” Checking if application exists: $APP_NAME" +if ! cpflow exists -a "$APP_NAME" --org "$CPLN_ORG"; then + echo "โš ๏ธ Application does not exist: $APP_NAME" + exit 0 +fi + +# Delete the application +echo "๐Ÿ—‘๏ธ Deleting application: $APP_NAME" +if ! cpflow delete -a "$APP_NAME" --org "$CPLN_ORG" --yes; then + echo "โŒ Failed to delete application: $APP_NAME" >&2 + exit 1 +fi + +echo "โœ… Successfully deleted application: $APP_NAME" diff --git a/.github/actions/setup-environment/action.yml b/.github/actions/setup-environment/action.yml new file mode 100644 index 00000000..e9212d6e --- /dev/null +++ b/.github/actions/setup-environment/action.yml @@ -0,0 +1,51 @@ +# Control Plane GitHub Action + +name: 'Setup Environment' +description: 'Sets up Ruby, installs Control Plane CLI, cpflow gem, and sets up the default profile' + +inputs: + token: + description: 'Control Plane token' + required: true + org: + description: 'Control Plane organization' + required: true + +runs: + using: 'composite' + steps: + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3.4' + + - name: Install Control Plane CLI and cpflow gem + shell: bash + run: | + sudo npm install -g @controlplane/cli@3.3.1 + cpln --version + gem install cpflow -v 4.1.1 + cpflow --version + + - name: Setup Control Plane Profile + shell: bash + run: | + TOKEN="${{ inputs.token }}" + ORG="${{ inputs.org }}" + + if [ -z "$TOKEN" ]; then + echo " Error: Control Plane token not provided" + exit 1 + fi + + if [ -z "$ORG" ]; then + echo " Error: Control Plane organization not provided" + exit 1 + fi + + echo "Setting up Control Plane profile..." + echo "Organization: $ORG" + cpln profile update default --org "$ORG" --token "$TOKEN" + + echo "Setting up Docker login for Control Plane registry..." + cpln image docker-login --org "$ORG" diff --git a/.github/readme.md b/.github/readme.md new file mode 100644 index 00000000..3b10fedc --- /dev/null +++ b/.github/readme.md @@ -0,0 +1,85 @@ +# Developing and Testing Github Actions + +Testing Github Actions on an existing repository is tricky. + +The main issue boils down to the fact that Github Actions uses the workflow files in the branch where the event originates. This is fine for push events, but it becomes a problem when you want to test workflows that are triggered by comments on a pull request. + +Here's a summary of the behavior: + +Behavior of push and pull_request Events + 1. Push on a Branch: + โ€ข When you push changes to a branch (e.g., feature-branch), GitHub Actions uses the workflow files in that same branch. + โ€ข This is why changes to workflows work seamlessly when testing with push events. + 2. Pull Request Events: + โ€ข For pull_request events (e.g., a PR from feature-branch into master), GitHub Actions will always use the workflow files from the target branch (e.g., master), not the source branch (e.g., feature-branch). + โ€ข This is a security feature to prevent someone from introducing malicious code in a PR that modifies the workflow files themselves. + +Impact on Comment-Triggered Workflows + +When you want to trigger workflows via comments (issue_comment) in a pull request: + โ€ข The workflow code used will always come from the master branch (or the default branch), regardless of the branch where the PR originates. + โ€ข This means the PRโ€™s changes to the workflow wonโ€™t be used, and the action invoked by the comment will also use code from master. + +Workarounds to Test Comment-Triggered Workflows + +If you want to test workflows in a way that uses the changes in the pull request, here are your options: + +1. Use Push Events for Testing + โ€ข Test your changes on a branch with push triggers. + โ€ข Use workflow_dispatch to simulate the events you need (like invoking actions via comments). + +This allows you to confirm that your changes to the workflow file or actions behave as expected before merging into master. + +2. Merge the Workflow to master Temporarily + +If you absolutely need the workflow to run as part of a pull_request event: + 1. Merge your workflow changes into master temporarily. + 2. Open a PR to test your comment-triggered workflows. + 3. Revert the changes in master if necessary. + +This ensures the workflow changes are active in master while still testing with the pull_request context. + +3. Add Logic to Detect the Source Branch + +Use github.event.pull_request.head.ref to add custom logic in your workflow that behaves differently based on the source branch. + โ€ข Example: + +jobs: + test-pr: + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.head.ref == 'feature-branch' }} + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Debug + run: echo "Testing workflow changes in feature-branch" + +However, this still requires the workflow itself to exist in master. + +4. Use a Fork or a Temporary Repo + +Create a temporary repository or a fork to test workflows in isolation: + โ€ข Push your workflow changes to master in the test repository. + โ€ข Open a PR in the fork to test how workflows behave with issue_comment events and PR contexts. + +Once confirmed, you can replicate the changes in your main repository. + +6. Alternative Approach: Split Workflows + +If your workflow includes comment-based triggers (issue_comment), consider splitting your workflows: + โ€ข A base workflow in master that handles triggering. + โ€ข A test-specific workflow for validating changes on a branch. + +For example: + 1. The base workflow triggers when a comment like /run-tests is added. + 2. The test-specific workflow runs in response to the base workflow but uses the branchโ€™s code. + +Summary + โ€ข For push events: The branch-specific workflow is used, so testing changes is easy. + โ€ข For pull_request and issue_comment events: GitHub always uses workflows from the master branch, and thereโ€™s no direct way to bypass this. + +To test comment-triggered workflows: + 1. Use push or workflow_dispatch to validate changes. + 2. Merge workflow changes temporarily into master to test with pull_request events. + 3. Use tools like act for local simulation. diff --git a/.github/workflows/delete-review-app.yml b/.github/workflows/delete-review-app.yml new file mode 100644 index 00000000..0e9baacb --- /dev/null +++ b/.github/workflows/delete-review-app.yml @@ -0,0 +1,170 @@ +name: Delete Review App + +on: + pull_request: + types: [closed] + issue_comment: + types: [created] + workflow_dispatch: + inputs: + pr_number: + description: 'PR number of the review app targeted for deletion' + required: true + type: string + +permissions: + contents: read + deployments: write + pull-requests: write + issues: write + +env: + PREFIX: ${{ vars.REVIEW_APP_PREFIX }} + CPLN_ORG: ${{ vars.CPLN_ORG_STAGING }} + CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} + APP_NAME: ${{ vars.REVIEW_APP_PREFIX }}-${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number }} + +jobs: + Process-Delete-Command: + if: | + (github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.comment.body == '/delete-review-app') || + (github.event_name == 'pull_request' && + github.event.action == 'closed') || + github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Validate Required Secrets and Variables + shell: bash + run: | + missing=() + + # Check required secrets + if [ -z "$CPLN_TOKEN" ]; then + missing+=("Secret: CPLN_TOKEN_STAGING") + fi + + # Check required variables + if [ -z "$CPLN_ORG" ]; then + missing+=("Variable: CPLN_ORG_STAGING") + fi + + if [ -z "$PREFIX" ]; then + missing+=("Variable: REVIEW_APP_PREFIX") + fi + + if [ ${#missing[@]} -ne 0 ]; then + echo "Required secrets/variables are not set: ${missing[*]}" + exit 1 + fi + + - name: Setup Environment + uses: ./.github/actions/setup-environment + with: + org: ${{ env.CPLN_ORG }} + token: ${{ env.CPLN_TOKEN }} + + - name: Set shared functions + id: shared-functions + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('GET_CONSOLE_LINK', ` + function getConsoleLink(prNumber) { + return '๐ŸŽฎ [Control Plane Console](' + + 'https://console.cpln.io/console/org/' + process.env.CPLN_ORG + '/gvc/' + process.env.APP_NAME + '/-info)'; + } + `); + + - name: Setup Workflow URL + id: setup-workflow-url + uses: actions/github-script@v7 + with: + script: | + async function getWorkflowUrl(runId) { + // Get the current job ID + const jobs = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId + }); + + const currentJob = jobs.data.jobs.find(job => job.status === 'in_progress'); + const jobId = currentJob?.id; + + if (!jobId) { + console.log('Warning: Could not find current job ID'); + return `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + } + + return `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/job/${jobId}`; + } + + const workflowUrl = await getWorkflowUrl(context.runId); + core.exportVariable('WORKFLOW_URL', workflowUrl); + return { workflowUrl }; + + - name: Create Initial Delete Comment + id: create-delete-comment + uses: actions/github-script@v7 + with: + script: | + eval(process.env.GET_CONSOLE_LINK); + + let message = '๐Ÿ—‘๏ธ Starting app deletion'; + if ('${{ github.event_name }}' === 'pull_request') { + const merged = '${{ github.event.pull_request.merged }}' === 'true'; + message += merged ? ' (PR merged)' : ' (PR closed)'; + } + + const comment = await github.rest.issues.createComment({ + issue_number: process.env.PR_NUMBER, + owner: context.repo.owner, + repo: context.repo.repo, + body: '๐Ÿ—‘๏ธ Starting app deletion...' + }); + return { commentId: comment.data.id }; + + - name: Delete Review App + uses: ./.github/actions/delete-control-plane-app + with: + app_name: ${{ env.APP_NAME }} + cpln_org: ${{ vars.CPLN_ORG_STAGING }} + + - name: Update Delete Status + uses: actions/github-script@v7 + with: + script: | + eval(process.env.GET_CONSOLE_LINK); + + const success = '${{ job.status }}' === 'success'; + const prNumber = process.env.PR_NUMBER; + const cpConsoleUrl = `https://console.cpln.io/org/${process.env.CPLN_ORG}/workloads/${process.env.APP_NAME}`; + + const successMessage = [ + 'โœ… Review app for PR #' + prNumber + ' was successfully deleted', + '', + ' [View Completed Delete Logs](' + process.env.WORKFLOW_URL + ')', + '', + ' [Control Plane Organization](https://console.cpln.io/console/org/' + process.env.CPLN_ORG + '/-info)' + ].join('\n'); + + const failureMessage = [ + 'โŒ Review app for PR #' + prNumber + ' failed to be deleted', + '', + ' [View Delete Logs with Errors](' + process.env.WORKFLOW_URL + ')', + '', + getConsoleLink(prNumber) + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: ${{ fromJSON(steps.create-delete-comment.outputs.result).commentId }}, + body: success ? successMessage : failureMessage + }); diff --git a/.github/workflows/deploy-to-control-plane-review-app.yml b/.github/workflows/deploy-to-control-plane-review-app.yml new file mode 100644 index 00000000..d0357491 --- /dev/null +++ b/.github/workflows/deploy-to-control-plane-review-app.yml @@ -0,0 +1,364 @@ +name: Deploy PR Review App to Control Plane + +run-name: Deploy PR Review App - PR #${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }} + +# Controls when the workflow will run +on: + pull_request: + types: [opened, synchronize, reopened] + issue_comment: + types: [created] + workflow_dispatch: + inputs: + pr_number: + description: 'Pull Request number to deploy' + required: true + type: number + +env: + PREFIX: ${{ vars.REVIEW_APP_PREFIX }} + APP_NAME: ${{ vars.REVIEW_APP_PREFIX }}-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }} + CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} + CPLN_ORG: ${{ vars.CPLN_ORG_STAGING }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }} + +jobs: + deploy: + if: | + (github.event_name == 'pull_request') || + (github.event_name == 'push') || + (github.event_name == 'workflow_dispatch') || + (github.event_name == 'issue_comment' && + github.event.issue.pull_request && + contains(github.event.comment.body, '/deploy-review-app')) + runs-on: ubuntu-latest + steps: + - name: Initial Checkout + uses: actions/checkout@v4 + + - name: Get PR HEAD Ref + id: getRef + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # For push events, try to find associated PR first + if [[ "${{ github.event_name }}" == "push" ]]; then + PR_DATA=$(gh pr list --head "${{ github.ref_name }}" --json number,headRefName,headRefOid --jq '.[0]') + if [[ -n "$PR_DATA" ]]; then + PR_NUMBER=$(echo "$PR_DATA" | jq -r .number) + else + echo "No PR found for branch ${{ github.ref_name }}, skipping deployment" + echo "DO_DEPLOY=false" >> $GITHUB_ENV + exit 0 + fi + else + # Get PR number based on event type + case "${{ github.event_name }}" in + "workflow_dispatch") + PR_NUMBER="${{ github.event.inputs.pr_number }}" + ;; + "issue_comment") + PR_NUMBER="${{ github.event.issue.number }}" + ;; + "pull_request") + PR_NUMBER="${{ github.event.pull_request.number }}" + ;; + *) + echo "Error: Unsupported event type ${{ github.event_name }}" + exit 1 + ;; + esac + fi + + if [[ -z "$PR_NUMBER" ]]; then + echo "Error: Could not determine PR number" + echo "Event type: ${{ github.event_name }}" + echo "Event action: ${{ github.event.action }}" + echo "Ref name: ${{ github.ref_name }}" + echo "Available event data:" + echo "- PR number from inputs: ${{ github.event.inputs.pr_number }}" + echo "- PR number from issue: ${{ github.event.issue.number }}" + echo "- PR number from pull_request: ${{ github.event.pull_request.number }}" + exit 1 + fi + + # Get PR data + if [[ -z "$PR_DATA" ]]; then + PR_DATA=$(gh pr view "$PR_NUMBER" --json headRefName,headRefOid) + if [[ -z "$PR_DATA" ]]; then + echo "Error: PR DATA for PR #$PR_NUMBER not found" + echo "Event type: ${{ github.event_name }}" + echo "Event action: ${{ github.event.action }}" + echo "Ref name: ${{ github.ref_name }}" + echo "Attempted to fetch PR data with: gh pr view $PR_NUMBER" + exit 1 + fi + fi + + # Extract and set PR data + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV + echo "APP_NAME=${{ vars.REVIEW_APP_PREFIX }}-$PR_NUMBER" >> $GITHUB_ENV + echo "PR_REF=$(echo $PR_DATA | jq -r .headRefName)" >> $GITHUB_OUTPUT + echo "PR_SHA=$(echo $PR_DATA | jq -r .headRefOid)" >> $GITHUB_ENV + + - name: Checkout the correct ref + uses: actions/checkout@v4 + with: + ref: ${{ env.PR_SHA }} + + - name: Setup Environment + uses: ./.github/actions/setup-environment + with: + token: ${{ secrets.CPLN_TOKEN_STAGING }} + org: ${{ vars.CPLN_ORG_STAGING }} + + - name: Check if Review App Exists + id: check-app + env: + CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} + run: | + # First check if cpflow exists + if ! command -v cpflow &> /dev/null; then + echo "Error: cpflow command not found" + exit 1 + fi + + # Check if app exists and save state + if ! cpflow exists -a ${{ env.APP_NAME }}; then + echo "APP_EXISTS=false" >> $GITHUB_ENV + else + echo "APP_EXISTS=true" >> $GITHUB_ENV + fi + + - name: Validate Deployment Request + id: validate + run: | + # Skip validation if deployment is already disabled + if [[ "${{ env.DO_DEPLOY }}" == "false" ]]; then + echo "Skipping validation - deployment already disabled" + exit 0 + fi + + if ! [[ "${{ github.event_name }}" == "workflow_dispatch" || \ + "${{ github.event_name }}" == "issue_comment" || \ + "${{ github.event_name }}" == "pull_request" || \ + "${{ github.event_name }}" == "push" ]]; then + echo "Error: Unsupported event type ${{ github.event_name }}" + exit 1 + fi + + # Set DO_DEPLOY based on event type and conditions + if [[ "${{ github.event_name }}" == "pull_request" && \ + ("${{ github.event.action }}" == "opened" || \ + "${{ github.event.action }}" == "synchronize" || \ + "${{ github.event.action }}" == "reopened") ]]; then + echo "DO_DEPLOY=true" >> $GITHUB_ENV + elif [[ "${{ github.event_name }}" == "push" ]]; then + echo "DO_DEPLOY=true" >> $GITHUB_ENV + elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "DO_DEPLOY=true" >> $GITHUB_ENV + elif [[ "${{ github.event_name }}" == "issue_comment" ]]; then + if [[ "${{ github.event.issue.pull_request }}" ]]; then + # Trim spaces and check for exact command + COMMENT_BODY=$(echo "${{ github.event.comment.body }}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + if [[ "$COMMENT_BODY" == "/deploy-review-app" ]]; then + echo "DO_DEPLOY=true" >> $GITHUB_ENV + else + echo "DO_DEPLOY=false" >> $GITHUB_ENV + echo "Skipping deployment - comment '$COMMENT_BODY' does not match '/deploy-review-app'" + fi + else + echo "DO_DEPLOY=false" >> $GITHUB_ENV + echo "Skipping deployment for non-PR comment" + fi + fi + if [[ "${{ env.DO_DEPLOY }}" == "false" ]]; then + exit 0 + fi + + - name: Setup Control Plane App if Not Existing + if: env.APP_EXISTS == 'false' + env: + CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} + run: | + echo "๐Ÿ”ง Setting up new Control Plane app..." + cpflow setup-app -a ${{ env.APP_NAME }} --org ${{ vars.CPLN_ORG_STAGING }} + + - name: Create Initial Comment + uses: actions/github-script@v7 + id: create-comment + with: + script: | + const result = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: process.env.PR_NUMBER, + body: '๐Ÿš€ Starting deployment process...\n\n' + }); + core.setOutput('comment-id', result.data.id); + + - name: Set Deployment URLs + id: set-urls + uses: actions/github-script@v7 + with: + script: | + // Set workflow URL for logs + const getWorkflowUrl = async (runId) => { + const { data: run } = await github.rest.actions.getWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId + }); + + // Get the job ID for this specific job + const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId + }); + + const currentJob = jobs.jobs.find(job => job.name === context.job); + return `${run.html_url}/job/${currentJob.id}`; + }; + + const workflowUrl = await getWorkflowUrl(context.runId); + core.exportVariable('WORKFLOW_URL', workflowUrl); + core.exportVariable('CONSOLE_LINK', + '๐ŸŽฎ [Control Plane Console](' + + 'https://console.cpln.io/console/org/' + process.env.CPLN_ORG + '/gvc/' + process.env.APP_NAME + '/-info)' + ); + + - name: Initialize GitHub Deployment + uses: actions/github-script@v7 + id: init-deployment + with: + script: | + const ref = process.env.PR_SHA; + const environment = process.env.ENVIRONMENT_NAME || 'review-app'; + + const deployment = await github.rest.repos.createDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: ref, + environment: environment, + auto_merge: false, + required_contexts: [], + description: `Deployment for PR #${process.env.PR_NUMBER}` + }); + + // Create initial deployment status + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.data.id, + state: 'in_progress', + description: 'Deployment started' + }); + + return deployment.data.id; + + - name: Update Status - Building + uses: actions/github-script@v7 + with: + script: | + const buildingMessage = [ + '๐Ÿ—๏ธ Building Docker image for PR #${{ env.PR_NUMBER }}, commit ${{ env.PR_SHA }}', + '', + '๐Ÿ“ [View Build Logs](${{ env.WORKFLOW_URL }})', + '', + process.env.CONSOLE_LINK + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: ${{ steps.create-comment.outputs.comment-id }}, + body: buildingMessage + }); + + - name: Build Docker Image + id: build + uses: ./.github/actions/build-docker-image + with: + app_name: ${{ env.APP_NAME }} + org: ${{ vars.CPLN_ORG_STAGING }} + commit: ${{ env.PR_SHA }} + PR_NUMBER: ${{ env.PR_NUMBER }} + + - name: Update Status - Deploying + uses: actions/github-script@v7 + with: + script: | + const deployingMessage = [ + '๐Ÿš€ Deploying to Control Plane...', + '', + 'โณ Waiting for deployment to be ready...', + '', + '๐Ÿ“ [View Deploy Logs](${{ env.WORKFLOW_URL }})', + '', + process.env.CONSOLE_LINK + ].join('\n'); + + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: ${{ steps.create-comment.outputs.comment-id }}, + body: deployingMessage + }); + + - name: Deploy to Control Plane + run: cpflow deploy-image -a ${{ env.APP_NAME }} --run-release-phase --org ${{ vars.CPLN_ORG_STAGING }} --verbose + + - name: Retrieve App URL + id: workload + run: echo "WORKLOAD_URL=$(cpln workload get rails --gvc ${{ env.APP_NAME }} | tee | grep -oP 'https://[^[:space:]]*\.cpln\.app(?=\s|$)' | head -n1)" >> "$GITHUB_OUTPUT" + + - name: Update Status - Deployment Complete + uses: actions/github-script@v7 + with: + script: | + const prNumber = process.env.PR_NUMBER; + const appUrl = '${{ steps.workload.outputs.WORKLOAD_URL }}'; + const workflowUrl = process.env.WORKFLOW_URL; + const isSuccess = '${{ job.status }}' === 'success'; + + const consoleLink = process.env.CONSOLE_LINK; + + // Create GitHub deployment status + const deploymentStatus = { + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: ${{ steps.init-deployment.outputs.result }}, + state: isSuccess ? 'success' : 'failure', + environment_url: isSuccess ? appUrl : undefined, + log_url: workflowUrl, + environment: 'review' + }; + + await github.rest.repos.createDeploymentStatus(deploymentStatus); + + // Define messages based on deployment status + const successMessage = [ + 'โœ… Deployment complete for PR #' + prNumber + ', commit ' + '${{ env.PR_SHA }}', + '', + '๐Ÿš€ [Review App for PR #' + prNumber + '](' + appUrl + ')', + consoleLink, + '', + '๐Ÿ“‹ [View Completed Action Build and Deploy Logs](' + workflowUrl + ')' + ].join('\n'); + + const failureMessage = [ + 'โŒ Deployment failed for PR #' + prNumber + ', commit ' + '${{ env.PR_SHA }}', + '', + consoleLink, + '', + '๐Ÿ“‹ [View Deployment Logs with Errors](' + workflowUrl + ')' + ].join('\n'); + + // Update the existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: ${{ steps.create-comment.outputs.comment-id }}, + body: isSuccess ? successMessage : failureMessage + }); diff --git a/.github/workflows/deploy-to-control-plane-staging.yml b/.github/workflows/deploy-to-control-plane-staging.yml new file mode 100644 index 00000000..de2c0207 --- /dev/null +++ b/.github/workflows/deploy-to-control-plane-staging.yml @@ -0,0 +1,86 @@ +# Control Plane GitHub Action + +name: Deploy to Control Plane Staging +run-name: Deploy Control Plane Staging App + +# Controls when the workflow will run +on: + push: + branches: + - '*' + workflow_dispatch: + +# Convert the GitHub secret variables to environment variables for use by the Control Plane CLI +env: + APP_NAME: ${{ vars.STAGING_APP_NAME }} + CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} + CPLN_ORG: ${{ vars.CPLN_ORG_STAGING }} + STAGING_APP_BRANCH: ${{ vars.STAGING_APP_BRANCH }} + +concurrency: + group: deploy-staging + cancel-in-progress: true + +jobs: + + validate-branch: + runs-on: ubuntu-latest + outputs: + is_deployable: ${{ steps.check_branch.outputs.is_deployable }} + steps: + - name: Check if allowed branch + id: check_branch + run: | + if [[ -n "${STAGING_APP_BRANCH}" ]]; then + if [[ "${GITHUB_REF#refs/heads/}" == "${STAGING_APP_BRANCH}" ]]; then + echo "is_deployable=true" >> $GITHUB_OUTPUT + else + echo "Branch '${GITHUB_REF#refs/heads/}' is not the configured deployment branch '${STAGING_APP_BRANCH}'" + echo "is_deployable=false" >> $GITHUB_OUTPUT + fi + elif [[ "${GITHUB_REF}" == "refs/heads/main" || "${GITHUB_REF}" == "refs/heads/master" ]]; then + echo "is_deployable=true" >> $GITHUB_OUTPUT + else + echo "Branch '${GITHUB_REF#refs/heads/}' is not main/master (no STAGING_APP_BRANCH configured)" + echo "is_deployable=false" >> $GITHUB_OUTPUT + fi + + build: + needs: validate-branch + if: needs.validate-branch.outputs.is_deployable == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Environment + uses: ./.github/actions/setup-environment + with: + token: ${{ secrets.CPLN_TOKEN_STAGING }} + org: ${{ vars.CPLN_ORG_STAGING }} + + - name: Build Docker Image + id: build + uses: ./.github/actions/build-docker-image + with: + app_name: ${{ env.APP_NAME }} + org: ${{ vars.CPLN_ORG_STAGING }} + commit: ${{ github.sha }} + + deploy: + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Environment + uses: ./.github/actions/setup-environment + with: + token: ${{ secrets.CPLN_TOKEN_STAGING }} + org: ${{ vars.CPLN_ORG_STAGING }} + + - name: Deploy to Control Plane + run: cpflow deploy-image -a ${{ env.APP_NAME }} --run-release-phase --org ${{ vars.CPLN_ORG_STAGING }} --verbose diff --git a/.github/workflows/help-command.yml b/.github/workflows/help-command.yml new file mode 100644 index 00000000..51ce2566 --- /dev/null +++ b/.github/workflows/help-command.yml @@ -0,0 +1,151 @@ +name: Help Command + +on: + issue_comment: + types: [created] + workflow_dispatch: + inputs: + pr_number: + description: 'Pull Request number to post help comment on' + required: true + type: string + +permissions: + issues: write + pull-requests: write + +jobs: + help: + if: ${{ (github.event.issue.pull_request && github.event.comment.body == '/help') || github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-latest + + steps: + - name: Show Available Commands + uses: actions/github-script@v7 + with: + script: | + const sections = { + commands: { + deploy: { + title: '## `/deploy`', + purpose: '**Purpose:** Deploy a review app for your pull request', + details: [ + '**What it does:**', + '- Creates a new review app in Control Plane', + '- Deploys your changes to the review environment', + '- Provides a unique URL to preview your changes', + '- Shows build and deployment progress in real-time', + '', + '**Optional Configuration:**', + '- `WAIT_TIMEOUT`: Deployment timeout in seconds (default: 900)', + ' - Must be a positive integer', + ' - Example: `/deploy timeout=1800`' + ] + }, + destroy: { + title: '## `/destroy`', + purpose: '**Purpose:** Remove the review app for your pull request', + details: [ + '**What it does:**', + '- Deletes the review app from Control Plane', + '- Cleans up associated resources', + '- Updates PR with deletion status' + ] + } + }, + setup: { + title: '## Environment Setup', + sections: [ + { + title: '**Required Environment Secrets:**', + items: [ + '- `CPLN_TOKEN_STAGING`: Control Plane authentication token', + '- `CPLN_TOKEN_PRODUCTION`: Control Plane authentication token' + ] + }, + { + title: '**Required GitHub Actions Variables:**', + items: [ + '- `CPLN_ORG_STAGING`: Control Plane authentication token', + '- `CPLN_ORG_PRODUCTION`: Control Plane authentication token' + ] + }, + { + title: '**Required GitHub Actions Variables (these need to match your control_plane.yml file:**', + items: [ + '- `PRODUCTION_APP_NAME`: Control Plane production app name', + '- `STAGING_APP_NAME`: Control Plane staging app name', + '- `REVIEW_APP_PREFIX`: Control Plane review app prefix' + ] + } + ], + note: 'Optional: Configure `WAIT_TIMEOUT` in GitHub Actions variables to customize deployment timeout' + }, + integration: { + title: '## Control Plane Integration', + details: [ + '1. Review app naming convention:', + ' ```', + ' ${{ vars.REVIEW_APP_PREFIX }}-', + ' ```', + '2. Console URL: `https://console.cpln.io/console/org/{CPLN_ORG}/gvc/{APP_NAME}/-info`' + ] + }, + cleanup: { + title: '## Automatic Cleanup', + details: [ + 'Review apps are automatically destroyed when:', + '1. The pull request is closed', + '2. The `/destroy` command is used', + '3. A new deployment is requested (old one is cleaned up first)' + ] + }, + help: { + title: '## Need Help?', + details: [ + 'For additional assistance:', + '1. Check the [Control Plane documentation](https://docs.controlplane.com/)', + '2. Contact the infrastructure team', + '3. Open an issue in this repository' + ] + } + }; + + const generateHelpText = () => { + const parts = ['# Available Commands', '']; + + // Add commands + Object.values(sections.commands).forEach(cmd => { + parts.push(cmd.title, cmd.purpose, '', ...cmd.details, ''); + }); + + parts.push('---'); + + // Add setup section + parts.push(sections.setup.title, ''); + sections.setup.sections.forEach(section => { + parts.push(section.title, ...section.items, ''); + }); + parts.push(sections.setup.note, ''); + + // Add remaining sections + ['integration', 'cleanup', 'help'].forEach(section => { + parts.push(sections[section].title, '', ...sections[section].details, ''); + }); + + return parts.join('\n'); + }; + + const helpText = generateHelpText(); + + const prNumber = context.eventName === 'workflow_dispatch' + ? parseInt(context.payload.inputs.pr_number) + : context.issue.number; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: helpText + }); + \ No newline at end of file diff --git a/.github/workflows/js_test.yml b/.github/workflows/js_test.yml new file mode 100644 index 00000000..b88d4ba1 --- /dev/null +++ b/.github/workflows/js_test.yml @@ -0,0 +1,48 @@ +name: "JS CI" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node: [22.x] + ruby: [3.3.4] + + env: + RAILS_ENV: test + NODE_ENV: test + USE_COVERALLS: true + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Use Node.js ${{ matrix.node }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + cache: yarn + + - name: Install dependencies + run: | + bundle install + yarn install --frozen-lockfile --non-interactive --prefer-offline + + - name: Build i18n libraries + run: bundle exec rake react_on_rails:locale + + - name: Run js tests + run: bundle exec rake ci:js diff --git a/.github/workflows/lint_test.yml b/.github/workflows/lint_test.yml new file mode 100644 index 00000000..48dea7f3 --- /dev/null +++ b/.github/workflows/lint_test.yml @@ -0,0 +1,47 @@ +name: "Lint CI" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node: [22.x] + ruby: [3.3.4] + + env: + RAILS_ENV: test + NODE_ENV: test + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Use Node.js ${{ matrix.node }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + cache: yarn + + - name: Install dependencies + run: | + bundle install + yarn install --frozen-lockfile --non-interactive --prefer-offline + + - name: Build i18n libraries + run: bundle exec rake react_on_rails:locale + + - name: Run lint + run: bundle exec rake lint diff --git a/.github/workflows/nightly-remove-stale-review-apps.yml b/.github/workflows/nightly-remove-stale-review-apps.yml new file mode 100644 index 00000000..d57c3e6e --- /dev/null +++ b/.github/workflows/nightly-remove-stale-review-apps.yml @@ -0,0 +1,24 @@ +name: Nightly Remove Stale Review Apps and Images + +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + schedule: + - cron: '0 0 * * *' + +jobs: + remove-stale-review-apps: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Environment + uses: ./.github/actions/setup-environment + with: + token: ${{ secrets.CPLN_TOKEN_STAGING }} + org: ${{ vars.CPLN_ORG_STAGING }} + + - name: Delete Stale Review Apps + run: cpflow cleanup-stale-apps -a "qa-react-webpack-rails-tutorial" --yes diff --git a/.github/workflows/promote-staging-to-production.yml b/.github/workflows/promote-staging-to-production.yml new file mode 100644 index 00000000..b06dc044 --- /dev/null +++ b/.github/workflows/promote-staging-to-production.yml @@ -0,0 +1,46 @@ +name: Promote Staging to Production + +on: + workflow_dispatch: + inputs: + confirm_promotion: + description: 'Type "promote" to confirm promotion of staging to production' + required: true + type: string + +jobs: + promote-to-production: + runs-on: ubuntu-latest + if: github.event.inputs.confirm_promotion == 'promote' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Environment + uses: ./.github/actions/setup-environment + with: + token: ${{ secrets.CPLN_TOKEN_PRODUCTION }} + org: ${{ vars.CPLN_ORG_PRODUCTION }} + + - name: Copy Image from Staging + run: cpflow copy-image-from-upstream -a ${{ vars.PRODUCTION_APP_NAME }} -t ${{ secrets.CPLN_TOKEN_STAGING }} + + - name: Deploy Image to Production + run: cpflow deploy-image -a ${{ vars.PRODUCTION_APP_NAME }} --run-release-phase --org ${{ vars.CPLN_ORG_PRODUCTION }} + + - name: Create GitHub Release + if: success() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Get the current date in YYYY-MM-DD format + RELEASE_DATE=$(date '+%Y-%m-%d') + + # Create a release tag + RELEASE_TAG="production-${RELEASE_DATE}" + + # Create GitHub release + gh release create "${RELEASE_TAG}" \ + --title "Production Release ${RELEASE_DATE}" \ + --notes "๐Ÿš€ Production deployment on ${RELEASE_DATE}" diff --git a/.github/workflows/review-app-help.yml b/.github/workflows/review-app-help.yml new file mode 100644 index 00000000..027330a8 --- /dev/null +++ b/.github/workflows/review-app-help.yml @@ -0,0 +1,49 @@ +name: Show Quick Help on PR Creation + +on: + pull_request: + types: [opened] + +permissions: + issues: write + pull-requests: write + +jobs: + show-quick-help: + runs-on: ubuntu-latest + steps: + - name: Show Quick Reference + uses: actions/github-script@v7 + with: + script: | + try { + console.log('Creating quick reference message...'); + const helpMessage = [ + '# ๐Ÿš€ Quick Review App Commands', + '', + 'Welcome! Here are the commands you can use in this PR:', + '', + '### `/deploy-review-app`', + 'Deploy your PR branch for testing', + '', + '### `/delete-review-app`', + 'Remove the review app when done', + '', + '### `/help`', + 'Show detailed instructions, environment setup, and configuration options.', + '', + '---' + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: helpMessage + }); + + console.log('Quick reference posted successfully'); + } catch (error) { + console.error('Error posting quick reference:', error); + core.setFailed(`Failed to post quick reference: ${error.message}`); + } diff --git a/.github/workflows/rspec_test.yml b/.github/workflows/rspec_test.yml new file mode 100644 index 00000000..ff3a6022 --- /dev/null +++ b/.github/workflows/rspec_test.yml @@ -0,0 +1,87 @@ +name: "Rspec CI" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node: [22.x] + ruby: [3.3.4] + + services: + postgres: + image: postgres:11-alpine + ports: + - "5432:5432" + env: + POSTGRES_DB: rails_test + POSTGRES_USER: rails + POSTGRES_PASSWORD: password + + env: + RAILS_ENV: test + NODE_ENV: test + DATABASE_URL: "postgres://rails:password@localhost:5432/rails_test" + DRIVER: selenium_chrome + CHROME_BIN: /usr/bin/google-chrome + USE_COVERALLS: true + + steps: + - name: Install Chrome + uses: browser-actions/setup-chrome@latest + + - name: Check Chrome version + run: chrome --version + + - name: Check Chrome version + run: google-chrome --version + + - name: Set Display environment variable + run: "export DISPLAY=:99" + + - name: Checkout code + uses: actions/checkout@v3 + + - name: Use Ruby ${{ matrix.ruby }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Use Node.js ${{ matrix.node }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + cache: yarn + + - name: Install dependencies + run: | + bundle install + yarn install --frozen-lockfile --non-interactive --prefer-offline + + - name: Set up database schema + run: bin/rails db:schema:load + + - name: Build i18n libraries + run: bundle exec rake react_on_rails:locale + + - name: Build Rescript components + run: yarn res:build + + - name: Build shakapacker chunks + run: NODE_ENV=development bundle exec bin/shakapacker + + - name: Run rspec with xvfb + uses: coactions/setup-xvfb@v1 + with: + run: bundle exec rake ci:rspec + working-directory: ./ #optional + options: ":99 -ac -screen scn 1600x1200x16" diff --git a/.gitignore b/.gitignore index 867e7252..7567a8b7 100644 --- a/.gitignore +++ b/.gitignore @@ -19,21 +19,36 @@ node_modules npm-debug.log* /coverage +dump.rdb +.DS_Store # Ignore bundle dependencies -vendor/ruby +vendor/bundle # RVM gemset .ruby-gemset # Generated js bundles -/public/webpack/ +/public/packs +/public/packs-test # Rubymine/IntelliJ .idea spec/examples.txt +# Redis generated file +dump.rdb + # Ignore i18n-js client/app/libs/i18n/translations.js client/app/libs/i18n/default.js + +/yarn-error.log +yarn-debug.log* +.yarn-integrity + +lib/bs +/lib/ocaml + +client/app/bundles/comments/rescript/**/*.bs.js diff --git a/.nvmrc b/.nvmrc index 09a6d308..1d9b7831 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -7.8.0 +22.12.0 diff --git a/.overcommit.yml b/.overcommit.yml new file mode 100644 index 00000000..c2bc6865 --- /dev/null +++ b/.overcommit.yml @@ -0,0 +1,33 @@ +# Use this file to configure the Overcommit hooks you wish to use. This will +# extend the default configuration defined in: +# https://github.com/sds/overcommit/blob/master/config/default.yml +# +# At the topmost level of this YAML file is a key representing type of hook +# being run (e.g. pre-commit, commit-msg, etc.). Within each type you can +# customize each hook, such as whether to only run it on certain files (via +# `include`), whether to only display output if it fails (via `quiet`), etc. +# +# For a complete list of hooks, see: +# https://github.com/sds/overcommit/tree/master/lib/overcommit/hook +# +# For a complete list of options that you can use to customize hooks, see: +# https://github.com/sds/overcommit#configuration +# +# Uncomment the following lines to make the configuration take effect. + +#PreCommit: +# RuboCop: +# enabled: true +# on_warn: fail # Treat all warnings as failures +# +# TrailingWhitespace: +# enabled: true +# exclude: +# - '**/db/structure.sql' # Ignore trailing whitespace in generated files +# +#PostCheckout: +# ALL: # Special hook name that customizes all hooks of this type +# quiet: true # Change all post-checkout hooks to only display output on failure +# +# IndexTags: +# enabled: true # Generate a tags file with `ctags` each time HEAD changes diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..0ffec769 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +node_modules/ +package.json +tmp/ +public/ +coverage/ +spec/support/ +client/app/libs/i18n/translations.js +client/app/libs/i18n/default.js +vendor/bundle diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 00000000..1254e0fb --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,18 @@ +printWidth: 110 +tabWidth: 2 +useTabs: false +semi: true +singleQuote: true +trailingComma: all +bracketSpacing: true + +overrides: +- files: "*.@(css|scss)" + options: + parser: css + singleQuote: false + printWidth: 120 +- files: "*.@(json)" + options: + parser: json + printWidth: 100 diff --git a/.rubocop.yml b/.rubocop.yml index 662a3e10..4b005bab 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,13 +1,21 @@ # This is the configuration used to check the rubocop source code. # Check out: https://github.com/bbatsov/rubocop +require: + - rubocop-performance + - rubocop-rspec + - rubocop-rails + AllCops: + NewCops: enable DisplayCopNames: true - TargetRubyVersion: 2.2 + TargetRubyVersion: 2.7 Include: - '**/Rakefile' - '**/config.ru' - - '**/Gemfile' + - 'Gemfile' + - '**/*.rb' + - '**/*.rake' Exclude: - 'vendor/**/*' - 'spec/fixtures/**/*' @@ -15,22 +23,88 @@ AllCops: - 'db/**/*' - 'db/schema.rb' - 'db/seeds.rb' - - 'client/node_modules/**/*' - 'bin/**/*' - 'Procfile.*' - - !ruby/regexp /old_and_unused\.rb$/ -Metrics/LineLength: +Layout/LineLength: Max: 120 -Metrics/MethodLength: - Max: 25 - -Metrics/BlockLength: - Max: 105 - Style/StringLiterals: EnforcedStyle: double_quotes Style/Documentation: Enabled: false + +Style/HashEachMethods: + Enabled: true + +Style/HashTransformKeys: + Enabled: true + +Style/HashTransformValues: + Enabled: true + +Lint/AssignmentInCondition: + Exclude: + - 'bin/spring' + +Lint/SuppressedException: + Exclude: + - 'bin/rails' + - 'bin/rake' + +Metrics/AbcSize: + Max: 28 + +Metrics/CyclomaticComplexity: + Max: 7 + +Metrics/PerceivedComplexity: + Max: 10 + +Metrics/ClassLength: + Max: 150 + +Metrics/BlockLength: + Exclude: + - 'config/environments/development.rb' + - 'config/environments/production.rb' + - 'lib/tasks/linters.rake' + - 'spec/rails_helper.rb' + - 'spec/system/add_new_comment_spec.rb' + - 'spec/system/react_router_demo_spec.rb' + - 'lib/tasks/ci.rake' + +Metrics/ParameterLists: + Max: 5 + CountKeywordArgs: false + +Metrics/MethodLength: + Max: 41 + +Metrics/ModuleLength: + Max: 180 + +Naming/RescuedExceptionsVariableName: + Enabled: false + +RSpec/DescribeClass: + Enabled: false + +RSpec/ExampleLength: + Enabled: false + +RSpec/MessageSpies: + Enabled: false + +RSpec/NestedGroups: + Max: 4 + +RSpec/MessageChain: + Enabled: false + +RSpec/MultipleExpectations: + Enabled: false + +RSpec/MultipleMemoizedHelpers: + Max: 12 diff --git a/.ruby-version b/.ruby-version index 55bc9834..a0891f56 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-2.4.1 +3.3.4 diff --git a/.scss-lint.yml b/.scss-lint.yml index b39fbece..a5770d07 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -1,7 +1,6 @@ # See http://sass-guidelin.es/#zeros scss_files: - - 'app/assets/stylesheets/**/*.scss' - 'client/app/**/*.scss' exclude: 'client/node_modules/**' diff --git a/.travis.yml b/.travis.yml index 0dabfb60..1b700128 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,42 +1,53 @@ -language: - - ruby +language: ruby + rvm: - - 2.4.1 -sudo: false -notifications: - slack: shakacode:YvfXbuFMcFAHt6ZjABIs0KET -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-4.9 + - 2.7.1 + services: + - docker - postgresql + +notifications: + slack: shakacode:YvfXbuFMcFAHt6ZjABIs0KET + +cache: + bundler: true + directories: + - node_modules # NPM packages + yarn: true + +gemfile: Gemfile + env: global: - RAILS_ENV=test - - CXX=g++-4.9 + - NODE_ENV=test + - DRIVER=selenium_chrome + - CHROME_BIN=/usr/bin/google-chrome + - USE_COVERALLS=TRUE + before_install: - - mkdir $PWD/travis-phantomjs - - curl -sSL https://github.com/Medium/phantomjs/releases/download/v2.1.1/phantomjs-2.1.1-linux-x86_64.tar.bz2 -o $PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2 - - tar -xvf $PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2 -C $PWD/travis-phantomjs - - export PATH=$PWD/travis-phantomjs/phantomjs-2.1.1-linux-x86_64/bin:$PATH + - sudo apt-get update + - sudo apt-get install -y xvfb libappindicator1 fonts-liberation + - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb + - sudo dpkg -i google-chrome*.deb + - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen scn 1600x1200x16" + install: - - bundle install - - chromedriver-update - - nvm install stable && nvm alias default stable - - npm install npm@latest -g - - npm install -g yarn - - npm --version - - yarn install + - travis_retry gem install bundler -v '<2' # Ruby 2.2 and Rails 3.2 & 4.2 depend on bundler 1.x. + - travis_retry nvm install 12 + - node -v + - travis_retry npm i -g yarn + - travis_retry bundle install + - travis_retry yarn + - bundle + - yarn + - google-chrome --version - rake db:setup + - bundle exec rake react_on_rails:locale -# Tip: No need to run xvfb if running headless testing. However, we're going to start with -# Poltergeist and switch to selenium if a test fails. before_script: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start + - "export DISPLAY=:99" script: - bundle exec rake db:schema:load diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ed23da7..75972ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. See: [merged pull requests](https://github.com/shakacode/react-webpack-rails-tutorial/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Amerged). + +## 2025-01-22 +Improvements to control-plane-flow implementation. + + + ## [2.1.0] - 2016-03-06 ### Updated diff --git a/Gemfile b/Gemfile index d8053875..fa33f8fb 100644 --- a/Gemfile +++ b/Gemfile @@ -1,13 +1,16 @@ +# frozen_string_literal: true + source "https://rubygems.org" -ruby "2.4.1" +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +ruby "3.3.4" + +gem "react_on_rails", "14.1.0.rc.0" +gem "shakapacker", "8.0.0" -# # Bundle edge Rails instead: gem "rails", github: "rails/rails" gem "listen" -gem "rails", "5.0.3" - -# Note: We're using sqllite3 for development and testing -# gem "sqlite3", group: [:development, :test] +gem "rails", "~> 8.0" gem "pg" @@ -27,7 +30,7 @@ gem "coffee-rails" # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem "jbuilder" -gem "redis" +gem "redis", "~> 5.0" # bundle exec rake doc:rails generates the API under doc/api. gem "sdoc", group: :doc @@ -38,18 +41,17 @@ gem "sdoc", group: :doc # Use Rails Html Sanitizer for HTML sanitization gem "rails-html-sanitizer" -gem "react_on_rails", "8.0.1" - -gem "webpacker_lite", "2.0.4" - -# See https://github.com/sstephenson/execjs#readme for more supported runtimes -# mini_racer is probably faster than therubyracer -gem "mini_racer" - gem "autoprefixer-rails" gem "awesome_print" +# Needed until Ruby 3.3.4 is released https://github.com/ruby/ruby/pull/11006 +# Related issue: https://github.com/ruby/net-pop/issues/26 +# TODO: When Ruby 3.3.4 is released, upgrade Ruby and remove this line +gem "net-pop", github: "ruby/net-pop" + +gem "redcarpet" + # jquery as the JavaScript library has been moved under /client and managed by npm. # It is critical to not include any of the jquery gems when following this pattern or # else you might have multiple jQuery versions. @@ -67,20 +69,22 @@ group :development, :test do ################################################################################ # Manage application processes - gem "factory_girl_rails" + gem "factory_bot_rails" gem "foreman" ################################################################################ # Linters and Security - gem "rubocop", require: false - gem "ruby-lint", require: false + gem "rubocop", "1.69", require: false + gem "rubocop-performance", "~> 1.13" + gem "rubocop-rails" + gem "rubocop-rspec", "~> 3.3" # Critical that require: false be set! https://github.com/brigade/scss-lint/issues/278 - gem "brakeman", require: false - gem "bundler-audit", require: false gem "scss_lint", require: false ################################################################################ # Favorite debugging gems + gem "debug", ">= 1.0.0" + gem "pry" gem "pry-byebug" gem "pry-doc" @@ -96,14 +100,14 @@ end group :test do gem "capybara" gem "capybara-screenshot" - gem "capybara-webkit" - gem "chromedriver-helper" - gem "coveralls", require: false + gem "coveralls_reborn", "~> 0.25.0", require: false gem "database_cleaner" gem "generator_spec" gem "launchy" - gem "poltergeist" - gem "rspec-rails", "~> 3.6" - gem "rspec-retry" - gem "selenium-webdriver", "<3.0.0" + gem "rails_best_practices" + gem "rspec-rails", "~> 6.0.0" + gem "selenium-webdriver", "~> 4" end + +gem "stimulus-rails", "~> 1.3" +gem "turbo-rails", "~> 2.0" diff --git a/Gemfile.lock b/Gemfile.lock index 0786bf6b..f3a5bd9f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,330 +1,467 @@ +GIT + remote: https://github.com/ruby/net-pop.git + revision: e8d0afe2773b9eb6a23c39e9e437f6fc0fc7c733 + specs: + net-pop (0.1.2) + GEM remote: https://rubygems.org/ specs: - actioncable (5.0.3) - actionpack (= 5.0.3) - nio4r (>= 1.2, < 3.0) - websocket-driver (~> 0.6.1) - actionmailer (5.0.3) - actionpack (= 5.0.3) - actionview (= 5.0.3) - activejob (= 5.0.3) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (5.0.3) - actionview (= 5.0.3) - activesupport (= 5.0.3) - rack (~> 2.0) - rack-test (~> 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.0.3) - activesupport (= 5.0.3) + actioncable (8.0.0) + actionpack (= 8.0.0) + activesupport (= 8.0.0) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.0.0) + actionpack (= 8.0.0) + activejob (= 8.0.0) + activerecord (= 8.0.0) + activestorage (= 8.0.0) + activesupport (= 8.0.0) + mail (>= 2.8.0) + actionmailer (8.0.0) + actionpack (= 8.0.0) + actionview (= 8.0.0) + activejob (= 8.0.0) + activesupport (= 8.0.0) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.0.0) + actionview (= 8.0.0) + activesupport (= 8.0.0) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.0.0) + actionpack (= 8.0.0) + activerecord (= 8.0.0) + activestorage (= 8.0.0) + activesupport (= 8.0.0) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.0.0) + activesupport (= 8.0.0) builder (~> 3.1) - erubis (~> 2.7.0) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.0.3) - activesupport (= 5.0.3) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.0.0) + activesupport (= 8.0.0) globalid (>= 0.3.6) - activemodel (5.0.3) - activesupport (= 5.0.3) - activerecord (5.0.3) - activemodel (= 5.0.3) - activesupport (= 5.0.3) - arel (~> 7.0) - activesupport (5.0.3) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (~> 0.7) - minitest (~> 5.1) - tzinfo (~> 1.1) - addressable (2.5.1) - public_suffix (~> 2.0, >= 2.0.2) - archive-zip (0.7.0) - io-like (~> 0.3.0) - arel (7.1.4) - ast (2.3.0) - autoprefixer-rails (6.7.7.1) - execjs - awesome_print (1.7.0) - bindex (0.5.0) - binding_of_caller (0.7.2) - debug_inspector (>= 0.0.1) - brakeman (3.6.1) - builder (3.2.3) - bundler-audit (0.5.0) - bundler (~> 1.2) - thor (~> 0.18) - byebug (9.0.6) - capybara (2.14.0) + activemodel (8.0.0) + activesupport (= 8.0.0) + activerecord (8.0.0) + activemodel (= 8.0.0) + activesupport (= 8.0.0) + timeout (>= 0.4.0) + activestorage (8.0.0) + actionpack (= 8.0.0) + activejob (= 8.0.0) + activerecord (= 8.0.0) + activesupport (= 8.0.0) + marcel (~> 1.0) + activesupport (8.0.0) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.2) + autoprefixer-rails (10.4.16.0) + execjs (~> 2) + awesome_print (1.9.2) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.8) + bindex (0.8.1) + binding_of_caller (1.0.1) + debug_inspector (>= 1.2.0) + builder (3.3.0) + byebug (11.1.3) + capybara (3.40.0) addressable - mime-types (>= 1.16) - nokogiri (>= 1.3.3) - rack (>= 1.0.0) - rack-test (>= 0.5.4) - xpath (~> 2.0) - capybara-screenshot (1.0.14) - capybara (>= 1.0, < 3) + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + capybara-screenshot (1.0.26) + capybara (>= 1.0, < 4) launchy - capybara-webkit (1.1.0) - capybara (~> 2.0, >= 2.0.2) - json - childprocess (0.6.3) - ffi (~> 1.0, >= 1.0.11) - chromedriver-helper (1.1.0) - archive-zip (~> 0.7.0) - nokogiri (~> 1.6) - cliver (0.3.2) - coderay (1.1.1) - coffee-rails (4.2.1) + childprocess (5.0.0) + code_analyzer (0.5.5) + sexp_processor + coderay (1.1.3) + coffee-rails (5.0.0) coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.2.x) + railties (>= 5.2.0) coffee-script (2.4.1) coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.0.5) - connection_pool (2.2.1) - coveralls (0.8.20) - json (>= 1.8, < 3) - simplecov (~> 0.14.1) - term-ansicolor (~> 1.3) - thor (~> 0.19.4) - tins (~> 1.6) - database_cleaner (1.5.3) - debug_inspector (0.0.2) - diff-lcs (1.3) - docile (1.1.5) + concurrent-ruby (1.3.3) + connection_pool (2.4.1) + coveralls_reborn (0.25.0) + simplecov (>= 0.18.1, < 0.22.0) + term-ansicolor (~> 1.6) + thor (>= 0.20.3, < 2.0) + tins (~> 1.16) + crass (1.0.6) + database_cleaner (2.0.2) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.1.0) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0.0) + database_cleaner-core (2.0.1) + date (3.4.0) + debug (1.9.2) + irb (~> 1.10) + reline (>= 0.3.8) + debug_inspector (1.2.0) + diff-lcs (1.5.1) + docile (1.4.0) + drb (2.2.1) + erubi (1.13.0) erubis (2.7.0) - execjs (2.7.0) - factory_girl (4.8.0) + execjs (2.9.1) + factory_bot (6.4.6) + activesupport (>= 5.0.0) + factory_bot_rails (6.4.3) + factory_bot (~> 6.4) + railties (>= 5.0.0) + ffi (1.17.0) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86_64-linux-gnu) + foreman (0.88.1) + generator_spec (0.10.0) activesupport (>= 3.0.0) - factory_girl_rails (4.8.0) - factory_girl (~> 4.8.0) railties (>= 3.0.0) - ffi (1.9.18) - foreman (0.84.0) - thor (~> 0.19.1) - generator_spec (0.9.3) - activesupport (>= 3.0.0) - railties (>= 3.0.0) - globalid (0.4.0) - activesupport (>= 4.2.0) - i18n (0.8.4) + globalid (1.2.1) + activesupport (>= 6.1) + i18n (1.14.5) + concurrent-ruby (~> 1.0) interception (0.5) - io-like (0.3.0) - jbuilder (2.6.3) - activesupport (>= 3.0.0, < 5.2) - multi_json (~> 1.2) - json (1.8.6) - launchy (2.4.3) - addressable (~> 2.3) - libv8 (5.3.332.38.5) - listen (3.1.5) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) - loofah (2.0.3) - nokogiri (>= 1.5.9) - mail (2.6.5) - mime-types (>= 1.16, < 4) - method_source (0.8.2) - mime-types (3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) - mini_portile2 (2.1.0) - mini_racer (0.1.9) - libv8 (~> 5.3) - minitest (5.10.2) - multi_json (1.12.1) - nio4r (2.0.0) - nokogiri (1.7.2) - mini_portile2 (~> 2.1.0) - parser (2.4.0.0) - ast (~> 2.2) - pg (0.20.0) - poltergeist (1.15.0) - capybara (~> 2.1) - cliver (~> 0.3.1) - websocket-driver (>= 0.2.0) - powerpack (0.1.1) - pry (0.10.4) - coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - pry-byebug (3.4.2) - byebug (~> 9.0) - pry (~> 0.10) - pry-doc (0.10.0) - pry (~> 0.9) - yard (~> 0.9) - pry-rails (0.3.6) - pry (>= 0.10.4) - pry-rescue (1.4.5) + io-console (0.7.2) + irb (1.13.2) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.12.0) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) + json (2.7.2) + language_server-protocol (3.17.0.3) + launchy (3.0.1) + addressable (~> 2.8) + childprocess (~> 5.0) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.0) + loofah (2.22.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + matrix (0.4.2) + method_source (1.1.0) + mini_mime (1.1.5) + mini_portile2 (2.8.7) + minitest (5.24.1) + mize (0.4.1) + protocol (~> 2.0) + net-imap (0.5.1) + date + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.0) + net-protocol + nio4r (2.7.3) + nokogiri (1.16.6) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.16.6-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.6-x86_64-linux) + racc (~> 1.4) + package_json (0.1.0) + parallel (1.26.3) + parser (3.3.3.0) + ast (~> 2.4.1) + racc + pg (1.5.6) + protocol (2.0.0) + ruby_parser (~> 3.0) + pry (0.14.2) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.10.1) + byebug (~> 11.0) + pry (>= 0.13, < 0.15) + pry-doc (1.5.0) + pry (~> 0.11) + yard (~> 0.9.11) + pry-rails (0.3.11) + pry (>= 0.13.0) + pry-rescue (1.6.0) interception (>= 0.5) - pry - pry-stack_explorer (0.4.9.2) - binding_of_caller (>= 0.7) - pry (>= 0.9.11) - public_suffix (2.0.5) - puma (3.8.2) - rack (2.0.3) - rack-test (0.6.3) - rack (>= 1.0) - rails (5.0.3) - actioncable (= 5.0.3) - actionmailer (= 5.0.3) - actionpack (= 5.0.3) - actionview (= 5.0.3) - activejob (= 5.0.3) - activemodel (= 5.0.3) - activerecord (= 5.0.3) - activesupport (= 5.0.3) - bundler (>= 1.3.0, < 2.0) - railties (= 5.0.3) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + pry (>= 0.12.0) + pry-stack_explorer (0.6.1) + binding_of_caller (~> 1.0) + pry (~> 0.13) + psych (5.1.2) + stringio + public_suffix (6.0.0) + puma (6.4.2) + nio4r (~> 2.0) + racc (1.8.0) + rack (3.1.4) + rack-proxy (0.7.7) + rack + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails (8.0.0) + actioncable (= 8.0.0) + actionmailbox (= 8.0.0) + actionmailer (= 8.0.0) + actionpack (= 8.0.0) + actiontext (= 8.0.0) + actionview (= 8.0.0) + activejob (= 8.0.0) + activemodel (= 8.0.0) + activerecord (= 8.0.0) + activestorage (= 8.0.0) + activesupport (= 8.0.0) + bundler (>= 1.15.0) + railties (= 8.0.0) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.0.3) - loofah (~> 2.0) - railties (5.0.3) - actionpack (= 5.0.3) - activesupport (= 5.0.3) - method_source - rake (>= 0.8.7) - thor (>= 0.18.1, < 2.0) - rainbow (2.2.2) - rake - rake (12.0.0) - rb-fsevent (0.9.8) - rb-inotify (0.9.8) - ffi (>= 0.5.0) - rdoc (4.3.0) - react_on_rails (8.0.1) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + rails_best_practices (1.23.2) + activesupport + code_analyzer (~> 0.5.5) + erubis + i18n + json + require_all (~> 3.0) + ruby-progressbar + railties (8.0.0) + actionpack (= 8.0.0) + activesupport (= 8.0.0) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rdoc (6.7.0) + psych (>= 4.0.0) + react_on_rails (14.1.0.rc.0) addressable connection_pool execjs (~> 2.5) - rails (>= 3.2) - rainbow (~> 2.1) - redis (3.3.3) - rspec-core (3.6.0) - rspec-support (~> 3.6.0) - rspec-expectations (3.6.0) + rails (>= 5.2) + rainbow (~> 3.0) + redcarpet (3.6.0) + redis (5.3.0) + redis-client (>= 0.22.0) + redis-client (0.22.2) + connection_pool + regexp_parser (2.9.2) + reline (0.5.9) + io-console (~> 0.5) + require_all (3.0.0) + rexml (3.3.1) + strscan + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-mocks (3.6.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-rails (3.6.0) - actionpack (>= 3.0) - activesupport (>= 3.0) - railties (>= 3.0) - rspec-core (~> 3.6.0) - rspec-expectations (~> 3.6.0) - rspec-mocks (~> 3.6.0) - rspec-support (~> 3.6.0) - rspec-retry (0.5.4) - rspec-core (> 3.3, < 3.7) - rspec-support (3.6.0) - rubocop (0.48.1) - parser (>= 2.3.3.1, < 3.0) - powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) + rspec-support (~> 3.13.0) + rspec-rails (6.0.4) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + rspec-support (3.13.1) + rubocop (1.69.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.4, < 3.0) + rubocop-ast (>= 1.36.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) - ruby-lint (2.3.1) - parser (~> 2.2) - slop (~> 3.4, >= 3.4.7) - ruby-progressbar (1.8.1) - ruby_dep (1.5.0) - rubyzip (1.2.1) - sass (3.4.23) - sass-rails (5.0.6) - railties (>= 4.0.0, < 6) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) - scss_lint (0.52.0) - rake (>= 0.9, < 13) - sass (~> 3.4.20) - sdoc (0.4.2) - json (~> 1.7, >= 1.7.7) - rdoc (~> 4.0) - selenium-webdriver (2.53.4) - childprocess (~> 0.5) - rubyzip (~> 1.0) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.36.1) + parser (>= 3.3.1.0) + rubocop-performance (1.19.1) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rails (2.15.2) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.7.0, < 2.0) + rubocop-rspec (3.3.0) + rubocop (~> 1.61) + ruby-progressbar (1.13.0) + ruby_parser (3.21.0) + racc (~> 1.5) + sexp_processor (~> 4.16) + rubyzip (2.3.2) + sass (3.7.4) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) + sassc (2.4.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt + scss_lint (0.60.0) + sass (~> 3.5, >= 3.5.5) + sdoc (2.6.1) + rdoc (>= 5.0) + securerandom (0.3.2) + selenium-webdriver (4.22.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - simplecov (0.14.1) - docile (~> 1.1.0) - json (>= 1.8, < 3) - simplecov-html (~> 0.10.0) - simplecov-html (0.10.0) - slop (3.6.0) - spring (2.0.1) - activesupport (>= 4.2) + semantic_range (3.0.0) + sexp_processor (4.17.1) + shakapacker (8.0.0) + activesupport (>= 5.2) + package_json + rack-proxy (>= 0.6.1) + railties (>= 5.2) + semantic_range (>= 2.3.0) + simplecov (0.21.2) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.4) + spring (4.2.1) spring-commands-rspec (1.0.4) spring (>= 0.9.1) - sprockets (3.7.1) + sprockets (4.2.1) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.0) - actionpack (>= 4.0) - activesupport (>= 4.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.1) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) - term-ansicolor (1.5.0) + stimulus-rails (1.3.3) + railties (>= 6.0.0) + stringio (3.1.1) + strscan (3.1.0) + sync (0.5.0) + term-ansicolor (1.10.2) + mize tins (~> 1.0) - thor (0.19.4) - thread_safe (0.3.6) - tilt (2.0.7) - tins (1.13.2) - tzinfo (1.2.3) - thread_safe (~> 0.1) - uglifier (3.1.13) + thor (1.3.1) + tilt (2.4.0) + timeout (0.4.1) + tins (1.33.0) + bigdecimal + sync + turbo-rails (2.0.11) + actionpack (>= 6.0.0) + railties (>= 6.0.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uglifier (4.2.0) execjs (>= 0.3.0, < 3) - unicode-display_width (1.1.3) - web-console (3.5.0) - actionview (>= 5.0) - activemodel (>= 5.0) + unicode-display_width (3.1.2) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + uri (1.0.2) + useragent (0.16.10) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) bindex (>= 0.4.0) - railties (>= 5.0) - webpacker_lite (2.0.4) - activesupport (>= 4.2) - multi_json (~> 1.2) - railties (>= 4.2) - websocket (1.2.4) - websocket-driver (0.6.5) + railties (>= 6.0.0) + webrick (1.8.1) + websocket (1.2.10) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.2) - xpath (2.1.0) - nokogiri (~> 1.3) - yard (0.9.8) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + yard (0.9.36) + zeitwerk (2.6.16) PLATFORMS + arm64-darwin-22 ruby + x86_64-linux DEPENDENCIES autoprefixer-rails awesome_print - brakeman - bundler-audit capybara capybara-screenshot - capybara-webkit - chromedriver-helper coffee-rails - coveralls + coveralls_reborn (~> 0.25.0) database_cleaner - factory_girl_rails + debug (>= 1.0.0) + factory_bot_rails foreman generator_spec jbuilder launchy listen - mini_racer + net-pop! pg - poltergeist pry pry-byebug pry-doc @@ -332,27 +469,32 @@ DEPENDENCIES pry-rescue pry-stack_explorer puma - rails (= 5.0.3) + rails (~> 8.0) rails-html-sanitizer + rails_best_practices rainbow - react_on_rails (= 8.0.1) - redis - rspec-rails (~> 3.6) - rspec-retry - rubocop - ruby-lint + react_on_rails (= 14.1.0.rc.0) + redcarpet + redis (~> 5.0) + rspec-rails (~> 6.0.0) + rubocop (= 1.69) + rubocop-performance (~> 1.13) + rubocop-rails + rubocop-rspec (~> 3.3) sass-rails scss_lint sdoc - selenium-webdriver (< 3.0.0) + selenium-webdriver (~> 4) + shakapacker (= 8.0.0) spring spring-commands-rspec + stimulus-rails (~> 1.3) + turbo-rails (~> 2.0) uglifier web-console - webpacker_lite (= 2.0.4) RUBY VERSION - ruby 2.4.1p111 + ruby 3.3.4p94 BUNDLED WITH - 1.15.1 + 2.4.17 diff --git a/Procfile.dev b/Procfile.dev index bf8dcebb..20453bbe 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,17 +1,7 @@ -app: echo "Use Procfile.static or uncomment this file and help us get Hot Reloading to work again" -# Basic procfile for dev work. -# Development is faster if you pick one of the other Procfiles if you don't need -# the processes to create the test files. Thus, run the `Procfile.hot` one instead - -# Development rails requires both rails and rails-assets -# (and rails-server-assets if server rendering) -# rails: HOT_RELOADING=TRUE rails s -b 0.0.0.0 - -# Run the hot reload server for client development -# hot-assets: sh -c 'rm -rf public/webpack/development || true && bundle exec rake react_on_rails:locale && HOT_RAILS_PORT=3500 yarn run hot-assets' - -# Render static client assets -# rails-static-client-assets: sh -c 'yarn run build:dev:client' - -# Render static client assets. Remove if not server rendering -# rails-static-server-assets: sh -c 'yarn run build:dev:server' +# Procfile for development using HMR +# You can run these commands in separate shells +rescript: yarn res:dev +redis: redis-server +rails: bundle exec rails s -p 3000 +wp-client: HMR=true RAILS_ENV=development NODE_ENV=development bin/shakapacker-dev-server +wp-server: bundle exec rake react_on_rails:locale && HMR=true SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch diff --git a/Procfile.dev-static b/Procfile.dev-static new file mode 100644 index 00000000..31ced8e2 --- /dev/null +++ b/Procfile.dev-static @@ -0,0 +1,10 @@ +# You can run these commands in separate shells +web: rails s -p 3000 +redis: redis-server + +# Next line runs a watch process with webpack to compile the changed files. +# When making frequent changes to client side assets, you will prefer building webpack assets +# upon saving rather than when you refresh your browser page. +# Note, if using React on Rails localization you will need to run +# `bundle exec rake react_on_rails:locale` before you run bin/shakapacker +webpack: sh -c 'bundle exec rake react_on_rails:locale && rm -rf public/packs/* || true && bin/shakapacker -w' diff --git a/Procfile.express b/Procfile.express deleted file mode 100644 index 88940b72..00000000 --- a/Procfile.express +++ /dev/null @@ -1,14 +0,0 @@ -# Only run the express server! -# Use this if you want to mock out your endpoints. -# It's a great way to prototype UI especially with non-Rails developers! -# You can still run tests, and they will build the webpack file for each test run. -# Hot reloading of JS and CSS is enabled via the webpack-dev-server - -# UPDATE: 2016-07-31 -# We no longer recommend using an express server with Rails. It's simply not necessary because: -# 1. Rails can hot reload -# 2. There's extra maintenance in keeping this synchronized. -# 3. React on Rails does not have a shared_store JS rendering: -# https://github.com/shakacode/react_on_rails/issues/504 - -express: sh -c 'HOT_PORT=4000 yarn start' diff --git a/Procfile.hot b/Procfile.hot deleted file mode 100644 index d4a8fabf..00000000 --- a/Procfile.hot +++ /dev/null @@ -1,12 +0,0 @@ -# Basic procfile for rails-related part of dev processes -# using a webpack development server to load JavaScript and CSS - -# Development rails requires both rails and rails-assets -# (and rails-server-assets if server rendering) -rails: HOT_RELOADING=TRUE rails s -b 0.0.0.0 - -# Run the hot reload server for client development -hot-assets: sh -c 'rm -rf public/webpack/development || true && bundle exec rake react_on_rails:locale && HOT_RAILS_PORT=3500 yarn run hot-assets' - -# Keep the JS fresh for server rendering. Remove if not server rendering -rails-server-assets: sh -c 'yarn run build:dev:server' diff --git a/Procfile.spec b/Procfile.spec deleted file mode 100644 index a51fbd05..00000000 --- a/Procfile.spec +++ /dev/null @@ -1,9 +0,0 @@ -# For keeping webpack bundles up-to-date during a testing workflow. -# If you don't keep this process going, you will rebuild the assets per spec run. This is configured -# in rails_helper.rb. - -# Build client assets, watching for changes. -rails-client-assets: sh -c 'bundle exec rake react_on_rails:locale && yarn run build:dev:client' - -# Build server assets, watching for changes. Remove if not server rendering. -rails-server-assets: sh -c 'yarn run build:dev:server' diff --git a/Procfile.static b/Procfile.static deleted file mode 100644 index 2f75941f..00000000 --- a/Procfile.static +++ /dev/null @@ -1,8 +0,0 @@ -# Run Rails without hot reloading (static assets). -rails: rails s -b 0.0.0.0 - -# Build client assets, watching for changes. -rails-client-assets: rm -rf public/webpack/development || true && bundle exec rake react_on_rails:locale && yarn run build:dev:client - -# Build server assets, watching for changes. Remove if not server rendering. -rails-server-assets: yarn run build:dev:server diff --git a/Procfile.static.trace b/Procfile.static.trace deleted file mode 100644 index feba3670..00000000 --- a/Procfile.static.trace +++ /dev/null @@ -1,8 +0,0 @@ -# Run Rails without hot reloading (static assets). -rails: TRACE_REACT_ON_RAILS=TRUE rails s -b 0.0.0.0 - -# Build client assets, watching for changes. -rails-client-assets: bundle exec rake react_on_rails:locale && yarn run build:dev:client - -# Build server assets, watching for changes. Remove if not server rendering. -rails-server-assets: yarn run build:dev:server diff --git a/README.md b/README.md index 912f6807..ba825675 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,63 @@ -[![Codeship Build Status](https://codeship.com/projects/287b26d0-0c05-0133-7a33-02e67aca5f06/status?branch=master)](https://app.codeship.com/projects/90975) [![Build Status](https://travis-ci.org/shakacode/react-webpack-rails-tutorial.svg?branch=code_coverage-linting)](https://travis-ci.org/shakacode/react-webpack-rails-tutorial) [![Dependency Status](https://gemnasium.com/shakacode/react-webpack-rails-tutorial.svg)](https://gemnasium.com/shakacode/react-webpack-rails-tutorial) [![Code Climate](https://codeclimate.com/github/shakacode/react-webpack-rails-tutorial/badges/gpa.svg)](https://codeclimate.com/github/shakacode/react-webpack-rails-tutorial) [![Coverage Status](https://coveralls.io/repos/shakacode/react-webpack-rails-tutorial/badge.svg?branch=master&service=github)](https://coveralls.io/github/shakacode/react-webpack-rails-tutorial?branch=master) + [![Code Climate](https://codeclimate.com/github/shakacode/react-webpack-rails-tutorial/badges/gpa.svg)](https://codeclimate.com/github/shakacode/react-webpack-rails-tutorial) [![Coverage Status](https://coveralls.io/repos/shakacode/react-webpack-rails-tutorial/badge.svg?branch=master&service=github)](https://coveralls.io/github/shakacode/react-webpack-rails-tutorial?branch=master) -*If this projects helps you, please give us a star!* +# React, Redux, Tailwind CSS, ES7, Webpack, Ruby on Rails Demo -## Thank you from Justin Gordon and [ShakaCode](http://www.shakacode.com) +* Server-Side Rendering of React via the [react_on_rails gem](https://github.com/shakacode/react_on_rails) +* Live at [www.reactrails.com](http://www.reactrails.com/) -Thank you for considering using [React on Rails](https://github.com/shakacode/react_on_rails). +## Control Plane Deployment Example -* **Video:** [Front-End Sadness to Happiness: The React on Rails Story](https://www.youtube.com/watch?v=SGkTvKRPYrk): History, motivations, philosophy, and overview. -* *[Click here for talk slides](http://www.shakacode.com/talks).* +[Control Plane](https://shakacode.controlplane.com) offers a viable, cost-saving alternative to Heroku, especially when using the [cpflow gem](https://rubygems.org/gems/cpflow) to deploy to Control Plane. -We at [ShakaCode](http://www.shakacode.com) are a small, boutique, remote-first application development company. We fund this project by: +ShakaCode recently migrated [HiChee.com](https://hichee.com) to Control Plane, resulting in a two-thirds reduction in server hosting costs! -* Providing priority support and training for anything related to React + Webpack + Rails in our [Coaching Program](http://www.shakacode.com/work/shakacode-coaching-plan.pdf). -* Building custom web and mobile (React Native) applications. We typically work with a technical founder or CTO and instantly provide a full development team including designers. -* Migrating **Angular** + Rails to React + Rails. You can see an example of React on Rails and our work converting Angular to React on Rails at [egghead.io](https://egghead.io/browse/frameworks). -* Augmenting your team to get your product completed more efficiently and quickly. +See doc in [./.controlplane/readme.md](./.controlplane/readme.md) for how to easily deploy this app to Control Plane. -My article "[Why Hire ShakaCode?](https://blog.shakacode.com/can-shakacode-help-you-4a5b1e5a8a63#.jex6tg9w9)" provides additional details about our projects. +The instructions leverage the `cpflow` CLI, with source code and many more tips on how to migrate from Heroku to Control Plane +in https://github.com/shakacode/heroku-to-control-plane. -If any of this resonates with you, please email me, [justin@shakacode.com](mailto:justin@shakacode.com). I offer a free half-hour project consultation, on anything from React on Rails to any aspect of web or mobile application development for both consumer and enterprise products. +---- -We are **[currently looking to hire](http://www.shakacode.com/about/#work-with-us)** like-minded developers that wish to work on our projects, including [Friends and Guests](https://www.friendsandguests.com). +## React on Rails Pro and ShakaCode Pro Support -I appreciate your attention and sharing of these offerings with anybody that we can help. Your support allows me to bring you and your team [front-end happiness in the Rails world](https://www.youtube.com/watch?v=SGkTvKRPYrk). +React on Rails Pro provides Node server rendering and other performance enhancements for React on Rails. -Aloha and best wishes from the ShakaCode team! +[![2018-09-11_10-31-11](https://user-images.githubusercontent.com/1118459/45467845-5bcc7400-b6bd-11e8-91e1-e0cf806d4ea4.png)](https://blog.shakacode.com/hvmns-90-reduction-in-server-response-time-from-react-on-rails-pro-eb08226687db) ------- +* [HVMN Testimonial, Written by Paul Benigeri, October 12, 2018](https://github.com/shakacode/react_on_rails/blob/master/docs/testimonials/hvmn.md) +* [HVMNโ€™s 90% Reduction in Server Response Time from React on Rails Pro](https://blog.shakacode.com/hvmns-90-reduction-in-server-response-time-from-react-on-rails-pro-eb08226687db) +* [Egghead React on Rails Pro Deployment Highlights](https://github.com/shakacode/react_on_rails/wiki/Egghead-React-on-Rails-Pro-Deployment-Highlights) + +For more information, see the [React on Rails Pro Docs](https://www.shakacode.com/react-on-rails-pro/). + +* Optimizing your front end setup with Webpack v5+ and Shakapacker for React on Rails including code splitting with loadable-components. +* Upgrading your app to use the current Webpack setup that skips the Sprockets asset pipeline. +* Better performance client and server side. -# Community -Please [**click to subscribe**](https://app.mailerlite.com/webforms/landing/l1d9x5) to keep in touch with Justin Gordon and [ShakaCode](http://www.shakacode.com/). I intend to send announcements of new releases of React on Rails and of our latest [blog articles](https://blog.shakacode.com) and tutorials. Subscribers will also have access to **exclusive content**, including tips and examples. +ShakaCode can also help you with your custom software development needs. We specialize in marketplace and e-commerce applications that utilize both Rails and React. We can even leverage our code for [HiChee.com](https://hichee.com) for your app! -[![2017-01-31_14-16-56](https://cloud.githubusercontent.com/assets/1118459/22490211/f7a70418-e7bf-11e6-9bef-b3ccd715dbf8.png)](https://app.mailerlite.com/webforms/landing/l1d9x5) +See the [ShakaCode Client Engagement Model](https://www.shakacode.com/blog/client-engagement-model/) article to learn how we can work together. + +------ + +## Community -* **Slack Room**: [Contact us](mailto:contact@shakacode.com) for an invite to the ShakaCode Slack room! Let us know if you want to contribute. * **[forum.shakacode.com](https://forum.shakacode.com)**: Post your questions -* **[@ShakaCode on Twitter](https://twitter.com/shakacode)** -* For a live, [open source](https://github.com/shakacode/react-webpack-rails-tutorial), example of this gem, see [www.reactrails.com](http://www.reactrails.com). +* **[@railsonmaui on Twitter](https://twitter.com/railsonmaui)** +* For a live, example of the code in this repo, see [www.reactrails.com](http://www.reactrails.com). ------ -# Testimonials +## Testimonials From Joel Hooks, Co-Founder, Chief Nerd at [egghead.io](https://egghead.io/), January 30, 2017: ![2017-01-30_11-33-59](https://cloud.githubusercontent.com/assets/1118459/22443635/b3549fb4-e6e3-11e6-8ea2-6f589dc93ed3.png) -For more testimonials, see [Live Projects](PROJECTS.md) and [Kudos](./KUDOS.md). +For more testimonials, see [Live Projects](https://github.com/shakacode/react_on_rails/blob/master/PROJECTS.md) and [Kudos](https://github.com/shakacode/react_on_rails/blob/master/KUDOS.md). ------- -# React on Rails Pro! -Justin is currently working with a couple contributors on some new private examples that incorporate ShakaCode's best practices for industrial strength apps using React on Rails. If you're interested in getting access to these and/or contributing, [email justin@shakacode.com](mailto:justin@shakacode.com). Technologies will include Webpack v2, Yarn, CSS Modules, Bootstrap v4, Redux-Saga, Normalizr, Reselect, etc. - -# Videos +## Videos ### [React On Rails Tutorial Series](https://www.youtube.com/playlist?list=PL5VAKH-U1M6dj84BApfUtvBjvF-0-JfEU) @@ -62,22 +67,7 @@ Justin is currently working with a couple contributors on some new private examp ## NEWS -* Project migrated to [webpacker_lite](https://github.com/shakacode/react-webpack-rails-tutorial/pull/395) and React on Rails 8.0.0. -* Action Cable was recently added in [PR #355](https://github.com/shakacode/react-webpack-rails-tutorial/pull/355). See [PR#360](https://github.com/shakacode/react-webpack-rails-tutorial/pull/360) for additional steps to make this work on Heroku. Note, you need to be running redis. We installed the free Heroku redis add-on. -* We made a react-native client: [shakacode/reactrails-react-native-client](https://github.com/shakacode/reactrails-react-native-client/). If you want to hack on this with us, [email justin@shakacode.com](mailto:justin@shakacode.com). -* We have [some other open PRs](https://github.com/shakacode/react-webpack-rails-tutorial/pulls) of things we may soon be incorporating, including localization and action cable! Stay tuned! If you have opinions of what should or should not get merged, get in touch with [justin@shakacode.com](mailto:justin@shakacode.com). - -This tutorial app demonstrates advanced functionality beyond what's provided by the React on Rails generators, mostly in the area of Webpack and React usage. Due to the architecture of placing all client side assets in the `/client` directory, React on Rails supports just about anything that Webpack and JavaScript can do, such as: - -1. **Handling of Sass and Bootstrap**: This tutorial uses [CSS modules via Webpack](https://github.com/css-modules/css-modules) so that all your client side configuration can be handled in a pure JavaScript tooling manner. This allows for hot reloading and a better separation of concerns (Rails for server-side functionality versus NPM/Webpack for all things client side). The alternative approach of using the traditional Rails Asset Pipeline for your CSS is simpler and supported by [React on Rails](https://github.com/shakacode/react_on_rails). _If you are looking for more information about using assets in your client JavaScript, check out the React on Rails docs: [Rails Assets](https://github.com/shakacode/react_on_rails/blob/master/docs/additional-reading/rails-assets.md) and [Webpack, the Asset Pipeline, and Using Assets w/ React](https://github.com/shakacode/react_on_rails/blob/master/docs/additional-reading/rails-assets-relative-paths.md). For real examples, look at the Webpack config files in the [client/](https://github.com/shakacode/react-webpack-rails-tutorial/tree/master/client) directory of this project, as well as some of the components that are using the client side assets (ex. [CommentScreen component](https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/client/app/bundles/comments/components/CommentScreen/CommentScreen.jsx))._ -1. **Hot Reloading with Rails**: If you want to implement hot reloading after using React on Rails generators, then see [Hot Reloading of Assets For Rails Development](https://github.com/shakacode/react_on_rails/blob/master/docs/additional-reading/hot-reloading-rails-development.md). The tutorial has different startup scripts than the generators. The dev mode has the WebapackDev server providing the JS and CSS assets to the tutorial. This means you get **HOT RELOADING** of your JS and CSS within your Rails app. - -# React, Redux, React-Bootstrap, ES7, Webpack, Rails Demo -## Server Rendering via the [react_on_rails gem](https://github.com/shakacode/react_on_rails) - -#### Live at [www.reactrails.com](http://www.reactrails.com/) -##### Check out our [react-native client for this project!](https://github.com/shakacode/react-native-tutorial) -This is a simple example application that illustrates the use of ReactJs to implement a commenting system. Front-end code leverages both ReactJs and Rails asset pipeline while the backend is 100% Rails. It shows off a little bit of the interactivity of a ReactJs application, allowing the commmenter to choose the form layout. `react-bootstrap` is used for the React components. A pure Rails UI generated from scaffolding is shown for comparison. +* 2022-01-11: Added example of deployment [to the ControlPlane](.controlplane/readme.md). You can see this tutorial live here: [http://reactrails.com/](http://reactrails.com/) @@ -87,13 +77,12 @@ You can see this tutorial live here: [http://reactrails.com/](http://reactrails. + [Technologies Involved](#technologies-involved) + [Basic Demo Setup](#basic-demo-setup) + [Basic Command Line](#basic-command-line) - + [Experimenting with Hot Reloading](#experimenting-with-hot-reloading-applies-to-both-procfilehot-and-procfileexpress) + [Javascript Development without Rails](#javascript-development-without-rails-using-the-webpack-dev-server) + [Rails Integration](#rails-integration) + [Webpack](#webpack) + [Configuration Files](#configuration-files) + [Additional Resources](#additional-resources) -+ [Sass, CSS Modules, and Twitter Bootstrap integration](#sass-css-modules-and-twitter-bootstrap-integration) ++ [Sass, CSS Modules, and Tailwind CSS integration](#sass-css-modules-and-tailwind-css-integration) + [Fonts with SASS](#fonts-with-sass) + [Process Management during Development](#process-management-during-development) + [Rendering with Express Server](#rendering-with-express-server) @@ -109,8 +98,8 @@ You can see this tutorial live here: [http://reactrails.com/](http://reactrails. - Example of React with [CSS Modules](http://glenmaddern.com/articles/css-modules) inside of Rails using Webpack as described in [Smarter CSS builds with Webpack](http://bensmithett.com/smarter-css-builds-with-webpack/). - Example of enabling hot reloading of both JS and CSS (modules) from your Rails app in development mode. Change your code. Save. Browser updates without a refresh! - Example of React/Redux with Rails Action Cable. -- Example of Rails 5 with ReactJs/Redux/React-Router with Webpack and ES7. -- Enabling development of a JS client independently from Rails using the [Webpack Dev Server](https://webpack.github.io/docs/webpack-dev-server.html). You can see this by starting the app and visiting http://localhost:4000 +- Example of Rails 7 with ReactJs/Redux/React-Router with Webpack and ES7. +- Enabling development of a JS client independently from Rails using the [Webpack Dev Server](https://webpack.js.org/configuration/dev-server/). You can see this by starting the app and visiting http://localhost:4000 - Enabling the use of npm modules and [Babel](https://babeljs.io/) with a Rails application using [Webpack](https://webpack.github.io/). - Easily enable retrofitting such a JS framework into an existing Rails app. You don't need a brand new single page app! - Example setting up Ruby and JavaScript linting in a real project, with corresponding CI rake tasks. @@ -122,80 +111,51 @@ See package.json and Gemfile for versions 1. [react_on_rails gem](https://github.com/shakacode/react_on_rails/) 1. [React](http://facebook.github.io/react/) -1. [react-bootstrap](https://react-bootstrap.github.io/) -1. [bootstrap-loader](https://www.npmjs.com/package/bootstrap-loader/) 1. [Redux](https://github.com/reactjs/redux) 1. [react-router](https://github.com/reactjs/react-router) 1. [react-router-redux](https://github.com/reactjs/react-router-redux) 1. [Webpack with hot-reload](https://github.com/webpack/docs/wiki/hot-module-replacement-with-webpack) (for local dev) 1. [Babel transpiler](https://github.com/babel/babel) -1. [Ruby on Rails 5](http://rubyonrails.org/) for backend app and comparison with plain HTML -1. [Heroku for Rails 5 deployment](https://devcenter.heroku.com/articles/getting-started-with-rails5) +1. [Ruby on Rails 7](http://rubyonrails.org/) for backend app and comparison with plain HTML +1. [Heroku for Rails 7 deployment](https://devcenter.heroku.com/articles/getting-started-with-rails7) +1. [Deployment to the ControlPlane](.controlplane/readme.md) 1. [Turbolinks 5](https://github.com/turbolinks/turbolinks) +1. [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) ## Basic Demo Setup -1. Be sure that you have Node installed! We suggest [nvm](https://github.com/creationix/nvm), with node version `v6.0` or above. See this article [Updating and using nvm](http://forum.shakacode.com/t/updating-and-using-nvm/293). + +### Prerequisites +- Node `v22.3.0` or above. Be sure that you have Node installed! We suggest using [nvm](https://github.com/creationix/nvm) and running `nvm list` to check the active Node version. See this article [Updating and using nvm](http://forum.shakacode.com/t/updating-and-using-nvm/293). +- Ruby 3.3.3 or above +- Postgres v9.2 or above +- Redis. Check that you have Redis installed by running `which redis-server`. If missing and on MacOS, install with Homebrew (`brew install redis`) +- [Yarn](https://yarnpkg.com/). + +### Setup 1. `git clone git@github.com:shakacode/react-webpack-rails-tutorial.git` 1. `cd react-webpack-rails-tutorial` -1. Check that you have Ruby 2.3.0 or greater -1. Check that you're using the right version of node. Run `nvm list` to check. Use 5.5 or greater. -1. Check that you have Postgres installed. Run `which postgres` to check. Use 9.4 or greater. -1. Check that you have `qmake` installed. Run `which qmake` to check. If missing, follow these instructions: [Installing Qt and compiling capybara-webkit](https://github.com/thoughtbot/capybara-webkit/wiki/Installing-Qt-and-compiling-capybara-webkit) -1. Check that you have Redis installed. Run `which redis-server` to check. If missing, install with Homebrew (`brew install redis`) or follow [these instructions](https://redis.io/topics/quickstart#installing-redis - ). 1. `bundle install` -1. `brew install yarn` 1. `yarn` 1. `rake db:setup` -1. `foreman start -f Procfile.hot` - 1. Open a browser tab to http://localhost:3000 for the Rails app example with HOT RELOADING - 2. Try Hot Reloading steps below! -1. `foreman start -f Procfile.static` - 1. Open a browser tab to http://localhost:3000 for the Rails app example. - 2. When you make changes, you have to refresh the browser page. +1. `rails start` + - Open a browser tab to http://localhost:3000 for the Rails app example ### Basic Command Line -1. Run all linters and tests: `rake` -1. See all npm commands: `yarn run` -1. To start all development processes: `foreman start -f Procfile.dev` -1. To start only all Rails development processes: `foreman start -f Procfile.hot` - -### Experimenting with Hot Reloading: applies to both `Procfile.hot` and `Procfile.express` -1. With the browser open to any JSX file, such as [client/app/bundles/comments/components/CommentBox/CommentBox.jsx](client/app/bundles/comments/components/CommentBox/CommentBox.jsx) and you can change the JSX code, hit save, and you will see the screen update without refreshing the window. This applies to port 3000 and port 4000. -1. Try changing a `.scss` file, such as a color in [client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.scss](client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.scss). You can see port 3000 or 4000 update automatically. -1. Be sure to take a look at the different Procfiles in this directory, as described below. - - -## Javascript development without Rails: using the Webpack Dev Server - -We include a sample setup for developing your JavaScript files without Rails. However, this is no longer recommended as it's best to create your APIs in Rails, and take advantage of the hot reloading of your react components provided by this project. - -1. Run the node server with file `server-express.js` with command `yarn run` or `cd client && node server-express.js`. -2. Point your browser to [http://localhost:4000](http://localhost:4000) - -Save a change to a JSX file and see it update immediately in the browser! Note, any browser state still exists, such as what you've typed in the comments box. That's totally different than [Live Reload](http://livereload.com/) which refreshes the browser. Note, we just got this working with your regular Rails server! See above for **Hot Loading**. +- Run all linters and tests: `rake` +- See all npm commands: `yarn run` +- To start all development processes: `foreman start -f Procfile.dev` +- To start only all Rails development processes: `foreman start -f Procfile.hot` ## Rails Integration **We're now using Webpack for all Sass and JavaScript assets so we can do CSS Modules within Rails!** -+ **Production Deployment**: We previously had created a file `lib/tasks/assets.rake` to modify the Rails precompile task to deploy assets for production. However, we add this automatically in newer versions of React on Rails. If you need to customize this file, see [lib/tasks/assets.rake from React on Rails](https://github.com/shakacode/react_on_rails/blob/master/lib/tasks/assets.rake) as an example as well as the doc file: [heroku-deployment.md](https://github.com/shakacode/react_on_rails/blob/master/docs/additional-reading/heroku-deployment.md). ++ **Production Deployment**: [heroku-deployment.md](https://github.com/shakacode/react_on_rails/blob/master/docs/additional-reading/heroku-deployment.md). + Configure Buildpacks ``` heroku buildpacks:set heroku/ruby --app your-app heroku buildpacks:add --index 1 heroku/nodejs --app your-app heroku buildpacks:set --index 3 https://github.com/sreid/heroku-buildpack-sourceversion.git --app your-app ``` -+ **Development Mode**: Two flavors: Hot reloading assets (JavaScript & CSS) and Static loading. - + **Hot Loading**: We modify the URL in [application.html.erb](app/views/layouts/application.html.erb) based on whether or not we're in production mode using the helpers `env_stylesheet_link_tag` and `env_javascript_include_tag`. *Development mode* uses the Webpack Dev server running on port 3500. Other modes (production/test) use precompiled files. See `Procfile.hot`. `Procfile.dev` also starts this mode. Note, *you don't have to refresh a Rails web page to view changes to JavaScript or CSS*. - - + **Static Loading**: This uses webpack to create physical files of the assets, both JavaScript and CSS. This is essentially what we had before we enabled *Hot Loading*. You have to *refresh* the page to see changes to JavaScript or CSS. See `Procfile.static`. It is important to note that tests will use the same statically generated files. - - + Note, the following view helpers do the magic to make this work: - ```erb - <%= env_stylesheet_link_tag(static: 'application_static', hot: 'application_non_webpack', options: { media: 'all', 'data-turbolinks-track' => true }) %> - <%= env_javascript_include_tag(hot: ['http://localhost:3500/vendor-bundle.js', 'http://localhost:3500/app-bundle.js']) %> - <%= env_javascript_include_tag(static: 'application_static', hot: 'application_non_webpack', options: { 'data-turbolinks-track' => true }) %> - ``` ## Testing + See [Yak Shaving Failing Integration Tests with React and Rails](https://blog.shakacode.com/yak-shaving-failing-integration-tests-with-react-a93444886c8c#.io9464uvz) @@ -206,26 +166,25 @@ Save a change to a JSX file and see it update immediately in the browser! Note, line in the `rails_helper.rb` file. If you are using this project as an example and are not using RSpec, you may want to implement similar logic in your own project. ## Webpack -### Configuration Files -- `webpack.client.base.config.js`: Common **client** configuration file to minimize code duplication for `webpack.client.rails.build.config`, `webpack.client.rails.hot.config`, `webpack.client.express.config` -- `webpack.client.express.config.js`: Webpack configuration for Express server [client/server-express.js](client/server-express.js) -- `webpack.client.rails.build.config.js`: Client side js bundle for deployment and tests. -- `webpack.client.rails.hot.config.js`: Webpack Dev Server bundler for serving rails assets on port 3500, used by [client/server-rails-hot.js](client/server-rails-hot.js), for hot reloading JS and CSS within Rails. -- `webpack.server.rails.build.config.js`: Server side js bundle, used by server rendering. +_Converted to use Shakapacker webpack configuration_. + ### Additional Resources -- [Webpack Docs](http://webpack.github.io/docs/) +- [Webpack Docs](https://webpack.js.org/) - [Webpack Cookbook](https://christianalfoni.github.io/react-webpack-cookbook/) - Good overview: [Pete Hunt's Webpack Howto](https://github.com/petehunt/webpack-howto) -## Sass, CSS Modules, and Twitter Bootstrap Integration +## Sass, CSS Modules, and Tailwind CSS Integration +This example project uses mainly Tailwind CSS for styling. +Besides this, it also demonstrates Sass and CSS modules, particularly for some CSS transitions. + We're using Webpack to handle Sass assets so that we can use CSS modules. The best way to understand how we're handling assets is to close follow this example. We'll be working on more docs soon. If you'd like to give us a hand, that's a great way to learn about this! For example in [client/app/bundles/comments/components/CommentBox/CommentBox.jsx](client/app/bundles/comments/components/CommentBox/CommentBox.jsx), see how we use standard JavaScript import syntax to refer to class names that come from CSS modules: ```javascript -import css from './CommentBox.scss'; +import css from './CommentBox.module.scss'; export default class CommentBox extends React.Component { render() { @@ -233,8 +192,8 @@ export default class CommentBox extends React.Component { const cssTransitionGroupClassNames = { enter: css.elementEnter, enterActive: css.elementEnterActive, - leave: css.elementLeave, - leaveActive: css.elementLeaveActive, + exit: css.elementLeave, + exitActive: css.elementLeaveActive, }; } } @@ -249,28 +208,7 @@ bundle exec foreman start -f ``` 1. [`Procfile.dev`](Procfile.dev): Starts the Webpack Dev Server and Rails with Hot Reloading. -2. [`Procfile.hot`](Procfile.hot): Starts the Rails server and the webpack server to provide hot reloading of assets, JavaScript and CSS. -3. [`Procfile.static`](Procfile.static): Starts the Rails server and generates static assets that are used for tests. -5. [`Procfile.spec`](Procfile.spec): Starts webpack to create the static files for tests. **Good to know:** If you want to start `rails s` separately to debug in `pry`, then run `Procfile.spec` to generate the assets and run `rails s` in a separate console. -6. [`Procfile.static.trace`](Procfile.static.trace): Same as `Procfile.static` but prints tracing information useful for debugging server rendering. -4. [`Procfile.express`](Procfile.express): Starts only the Webpack Dev Server for rendering your components with only an Express server. - -In general, you want to avoid running more webpack watch processes than you need. - -## Rendering with Express Server -UPDATE: 2016-07-31 - -We no longer recommend using an express server with Rails. It's simply not necessary because: - -1. Rails can hot reload -2. There's extra maintenance in keeping this synchronized. -3. React on Rails does not have a shared_store JS rendering, per [issue #504](https://github.com/shakacode/react_on_rails/issues/504) - -### Setup - -1. `foreman start -f Procfile.express` -2. Open a browser tab to http://localhost:4000 for the Hot Module Replacement Example just using an express server (no Rails involved). This is good for fast prototyping of React components. However, this setup is not as useful now that we have hot reloading working for Rails! -3. Try Hot Reloading steps below! +1. [`Procfile.static`](Procfile.dev-static): Starts the Rails server and generates static assets that are used for tests. ## Contributors [The Shaka Code team!](http://www.shakacode.com/about/), led by [Justin Gordon](https://github.com/justin808/), along with with many others. See [contributors.md](docs/contributors.md) @@ -278,41 +216,14 @@ We no longer recommend using an express server with Rails. It's simply not neces ### RubyMine and WebStorm Special thanks to [JetBrains](https://www.jetbrains.com) for their great tools: [RubyMine](https://www.jetbrains.com/ruby/) and [WebStorm](https://www.jetbrains.com/webstorm/). Some developers of this project use RubyMine at the top level, mostly for Ruby work, and we use WebStorm opened up to the `client` directory to focus on JSX and Sass files. -## Open Code of Conduct -This project adheres to the [Open Code of Conduct](http://todogroup.org/opencodeofconduct/#fetch/opensource@github.com). By participating, you are expected to uphold this code. - -## About [ShakaCode](http://www.shakacode.com/) -If you would like to know more about ShakaCode, please read [Who Is ShakaCode](http://www.shakacode.com/2015/09/17/who-is-shaka-code.html) and [Success the ShakaCode Way!](http://www.shakacode.com/2015/11/26/success-the-shakacode-way.html) - -Please visit [our forums!](http://forum.shakacode.com). We've got a [category dedicated to react_on_rails](http://forum.shakacode.com/c/rails/reactonrails). - -You can also join our slack room for some free advice. Email us for an invite. +### Hiring We're looking for great developers that want to work with Rails + React (and react-native!) with a remote-first, distributed, worldwide team, for our own products, client work, and open source. [More info here](http://www.shakacode.com/about/index.html#work-with-us). --- -*Identical to top of page* ## Thank you from Justin Gordon and [ShakaCode](http://www.shakacode.com) Thank you for considering using [React on Rails](https://github.com/shakacode/react_on_rails). -* **Video:** [Front-End Sadness to Happiness: The React on Rails Story](https://www.youtube.com/watch?v=SGkTvKRPYrk): History, motivations, philosophy, and overview. -* *[Click here for talk slides](http://www.shakacode.com/talks).* - -We at [ShakaCode](http://www.shakacode.com) are a small, boutique, remote-first application development company. We fund this project by: - -* Providing priority support and training for anything related to React + Webpack + Rails in our [Coaching Program](http://www.shakacode.com/work/shakacode-coaching-plan.pdf). -* Building custom web and mobile (React Native) applications. We typically work with a technical founder or CTO and instantly provide a full development team including designers. -* Migrating **Angular** + Rails to React + Rails. You can see an example of React on Rails and our work converting Angular to React on Rails at [egghead.io](https://egghead.io/browse/frameworks). -* Augmenting your team to get your product completed more efficiently and quickly. - -My article "[Why Hire ShakaCode?](https://blog.shakacode.com/can-shakacode-help-you-4a5b1e5a8a63#.jex6tg9w9)" provides additional details about our projects. - -If any of this resonates with you, please email me, [justin@shakacode.com](mailto:justin@shakacode.com). I offer a free half-hour project consultation, on anything from React on Rails to any aspect of web or mobile application development for both consumer and enterprise products. - -We are **[currently looking to hire](http://www.shakacode.com/about/#work-with-us)** like-minded developers that wish to work on our projects, including [Friends and Guests](https://www.friendsandguests.com). - -I appreciate your attention and sharing of these offerings with anybody that we can help. Your support allows me to bring you and your team [front-end happiness in the Rails world](https://www.youtube.com/watch?v=SGkTvKRPYrk). - Aloha and best wishes from the ShakaCode team! diff --git a/Rakefile b/Rakefile index 1d92159c..dbf84da9 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require File.expand_path("../config/application", __FILE__) +require File.expand_path("config/application", __dir__) Rails.application.load_tasks diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 00000000..5d3696a9 --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line no-empty,no-lone-blocks +{ +} diff --git a/app/assets/javascripts/application_non_webpack.js b/app/assets/javascripts/application_non_webpack.js deleted file mode 100644 index 8e3c3994..00000000 --- a/app/assets/javascripts/application_non_webpack.js +++ /dev/null @@ -1,3 +0,0 @@ -// All webpack assets in development will be loaded via webpack dev server - -// turbolinks comes from npm and is listed in webpack.client.base.config.js diff --git a/app/assets/javascripts/application_static.js b/app/assets/javascripts/application_static.js deleted file mode 100644 index 6cd2fca4..00000000 --- a/app/assets/javascripts/application_static.js +++ /dev/null @@ -1,14 +0,0 @@ -// This file is used in production to server generated JS assets. In development mode, we use the Webpack Dev Server -// to provide assets. This allows for hot reloading of the JS and CSS. -// See app/helpers/application_helper.rb for how the correct assets file is picked based on the Rails environment. -// Those helpers are used here: app/views/layouts/application.html.erb - -// These assets are located in app/assets/webpack directory -// CRITICAL that webpack/vendor-bundle must be BEFORE turbolinks -// since it is exposing jQuery and jQuery-ujs - -//= require vendor-bundle -//= require app-bundle - -// Non-webpack assets incl turbolinks -//= require application_non_webpack diff --git a/app/assets/stylesheets/application_non_webpack.scss b/app/assets/stylesheets/application_non_webpack.scss deleted file mode 100644 index 1a8e6ff6..00000000 --- a/app/assets/stylesheets/application_non_webpack.scss +++ /dev/null @@ -1,5 +0,0 @@ -// Any non webpack assets can be imported here -// Others will be served via webpack-dev-server -.something { - color: pink; -} diff --git a/app/assets/stylesheets/application_static.scss b/app/assets/stylesheets/application_static.scss deleted file mode 100644 index 3276322b..00000000 --- a/app/assets/stylesheets/application_static.scss +++ /dev/null @@ -1,10 +0,0 @@ -// These assets are located in app/assets/webpack directory, and are generated ONLY when static -// assets are prepared (not for hot reloading assets). - -// Super important: This file is loaded even for hot loading only, so we need to be sure -// that we don't reference the static generated CSS files. -@import 'vendor-bundle'; -@import 'app-bundle'; - -// Non-webpack assets -@import 'application_non_webpack'; diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb index d6726972..9aec2305 100644 --- a/app/channels/application_cable/channel.rb +++ b/app/channels/application_cable/channel.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ApplicationCable class Channel < ActionCable::Channel::Base end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 0ff5442f..8d6c2a1b 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ApplicationCable class Connection < ActionCable::Connection::Base end diff --git a/app/channels/comments_channel.rb b/app/channels/comments_channel.rb index cf1a1c53..21c16395 100644 --- a/app/channels/comments_channel.rb +++ b/app/channels/comments_channel.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CommentsChannel < ApplicationCable::Channel def subscribed stream_from "comments" diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 69b29033..410a327e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 66c87759..1a534a55 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -1,20 +1,21 @@ +# frozen_string_literal: true + class CommentsController < ApplicationController + layout "stimulus_layout" before_action :set_comment, only: %i[show edit update destroy] + before_action :new_comment, only: %i[new stimulus horizontal_form stacked_form inline_form] + before_action :set_comments, only: %i[index stimulus comment_list] # GET /comments # GET /comments.json - def index - @comments = Comment.all.order("id DESC") - end + def index; end # GET /comments/1 # GET /comments/1.json def show; end # GET /comments/new - def new - @comment = Comment.new - end + def new; end # GET /comments/1/edit def edit; end @@ -26,10 +27,18 @@ def create respond_to do |format| if @comment.save - format.html { redirect_to @comment, notice: "Comment was successfully created." } + if turbo_frame_request? + format.html + else + format.html { redirect_to @comment, notice: I18n.t(:comment_was_successfully_created) } + end format.json { render :show, status: :created, location: @comment } else - format.html { render :new } + if turbo_frame_request? + format.html + else + format.html { render :new } + end format.json { render json: @comment.errors, status: :unprocessable_entity } end end @@ -40,7 +49,7 @@ def create def update respond_to do |format| if @comment.update(comment_params) - format.html { redirect_to @comment, notice: "Comment was successfully updated." } + format.html { redirect_to @comment, notice: I18n.t(:comment_was_successfully_updated) } format.json { render :show, status: :ok, location: @comment } else format.html { render :edit } @@ -52,20 +61,59 @@ def update # DELETE /comments/1 # DELETE /comments/1.json def destroy - @comment.destroy + @comment.destroy! respond_to do |format| - format.html { redirect_to comments_url, notice: "Comment was successfully destroyed." } + format.html { redirect_to comments_url, notice: I18n.t(:comment_was_successfully_destroyed) } format.json { head :no_content } end end + def stimulus + @form_type = "horizontal" + end + + def comment_list + respond_to do |format| + format.html { render partial: "comments/turbo/comment_list" } + end + end + + def horizontal_form + @form_type = "horizontal" + respond_to do |format| + format.html { render partial: "comments/turbo/horizontal_form" } + end + end + + def stacked_form + @form_type = "stacked" + respond_to do |format| + format.html { render partial: "comments/turbo/stacked_form" } + end + end + + def inline_form + @form_type = "inline" + respond_to do |format| + format.html { render partial: "comments/turbo/inline_form" } + end + end + private + def set_comments + @comments = Comment.all.order("id DESC") + end + # Use callbacks to share common setup or constraints between actions. def set_comment @comment = Comment.find(params[:id]) end + def new_comment + @comment = Comment.new + end + # Never trust parameters from the scary internet, only allow the white list through. def comment_params params.require(:comment).permit(:author, :text) diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 3f3956c7..9336df90 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PagesController < ApplicationController include ReactOnRails::Controller before_action :set_comments @@ -34,6 +36,8 @@ def no_router def simple; end + def rescript; end + private def set_comments @@ -41,13 +45,11 @@ def set_comments end def comments_json_string - render_to_string(template: "/comments/index.json.jbuilder", - locals: { comments: Comment.all }, format: :json) + render_to_string(template: "/comments/index", + locals: { comments: Comment.all }, formats: :json) end def render_html - respond_to do |format| - format.html - end + respond_to(&:html) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be794..15b06f0f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + module ApplicationHelper end diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb index 0ec9ca5f..f702e05c 100644 --- a/app/helpers/comments_helper.rb +++ b/app/helpers/comments_helper.rb @@ -1,2 +1,11 @@ +# frozen_string_literal: true + module CommentsHelper + MarkdownToHtmlParser = Redcarpet::Markdown.new(Redcarpet::Render::HTML) + + def markdown_to_html(content) + return "" if content.blank? + + sanitize(MarkdownToHtmlParser.render(content)) + end end diff --git a/app/helpers/pages_helper.rb b/app/helpers/pages_helper.rb index abd6cdd8..51a9423f 100644 --- a/app/helpers/pages_helper.rb +++ b/app/helpers/pages_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module PagesHelper def git_commit_sha GitCommitSha.current_sha @@ -5,6 +7,6 @@ def git_commit_sha def git_commit_sha_short full_sha = git_commit_sha - full_sha[-7..-1] + full_sha[...7] end end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index a009ace5..d92ffddc 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class ApplicationJob < ActiveJob::Base end diff --git a/app/jobs/comment_relay_job.rb b/app/jobs/comment_relay_job.rb index bce0317e..16305945 100644 --- a/app/jobs/comment_relay_job.rb +++ b/app/jobs/comment_relay_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CommentRelayJob < ApplicationJob def perform(comment) ActionCable.server.broadcast "comments", comment unless comment.destroyed? diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 00000000..71fbba5b --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/comment.rb b/app/models/comment.rb index 90faf1e9..f4ca8873 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,4 +1,6 @@ -class Comment < ActiveRecord::Base +# frozen_string_literal: true + +class Comment < ApplicationRecord validates :author, :text, presence: true after_commit { CommentRelayJob.perform_later(self) } end diff --git a/app/models/git_commit_sha.rb b/app/models/git_commit_sha.rb index c8a80fc6..3c58498f 100644 --- a/app/models/git_commit_sha.rb +++ b/app/models/git_commit_sha.rb @@ -1,15 +1,17 @@ +# frozen_string_literal: true + # Retrieves the current git commit SHA of the project class GitCommitSha - def self.current_sha - @sha ||= retrieve_sha_from_file.presence || retrieve_sha_from_git - end + attr_writer :current_sha - def self.current_sha=(sha) - @sha = sha + def self.current_sha + @current_sha ||= ENV["GIT_COMMIT_SHA"].presence || + retrieve_sha_from_file.presence || + retrieve_sha_from_git end def self.reset_current_sha - self.current_sha = nil + @current_sha = nil end # Assumes the git CLI is available. This is not the case in production on Heroku. diff --git a/app/views/comments/_form.html.erb b/app/views/comments/_form.html.erb index 2c82f17c..150e6845 100644 --- a/app/views/comments/_form.html.erb +++ b/app/views/comments/_form.html.erb @@ -1,10 +1,10 @@ -<%= form_for(@comment) do |f| %> - <% if @comment.errors.any? %> -
-

<%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved:

+<%= form_for(comment, html: { class: "flex flex-col gap-4" }) do |f| %> + <% if comment.errors.any? %> +
+

<%= pluralize(comment.errors.count, "error") %> prohibited this comment from being saved:

    - <% @comment.errors.full_messages.each do |message| %> + <% comment.errors.full_messages.each do |message| %>
  • <%= message %>
  • <% end %>
@@ -13,13 +13,13 @@
<%= f.label :author, 'Your Name' %>
- <%= f.text_field :author %> + <%= f.text_field :author, class: "px-3 py-1 leading-4 border border-gray-300 rounded" %>
<%= f.label :text, 'Say something using markdown...' %>
- <%= f.text_area :text %> + <%= f.text_area :text, class: "px-3 py-1 leading-4 border border-gray-300 rounded" %>
- <%= f.submit 'Post' %> + <%= f.submit 'Post', class: "self-start px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800 cursor-pointer" %>
<% end %> diff --git a/app/views/comments/edit.html.erb b/app/views/comments/edit.html.erb index a86a8b01..4f72a412 100644 --- a/app/views/comments/edit.html.erb +++ b/app/views/comments/edit.html.erb @@ -1,6 +1,6 @@ -

Editing Comment

+

Editing Comment

-<%= render 'form' %> +<%= render 'form', comment: @comment %> <%= link_to 'Show', @comment %> | <%= link_to 'Back', comments_path %> diff --git a/app/views/comments/index.html.erb b/app/views/comments/index.html.erb index 0cf6de0e..92ae4960 100644 --- a/app/views/comments/index.html.erb +++ b/app/views/comments/index.html.erb @@ -1,32 +1,37 @@ -

Using Classic Rails 4.2 "generate scaffold"

-
+

+ Using Classic Rails 4.2 "generate scaffold" +

-

<%= notice %>

+
+ <% if notice %> +

<%= notice %>

+ <% end %> -

Listing Comments

+

Listing Comments

- - - - - - - - - - - <% @comments.each do |comment| %> +
AuthorText
+ - - - - - + + + - <% end %> - -
<%= comment.author %><%= comment.text %><%= link_to 'Show', comment %><%= link_to 'Edit', edit_comment_path(comment) %><%= link_to 'Destroy', comment, method: :delete, data: { confirm: 'Are you sure?' } %>AuthorText
+ -
+ + <% @comments.each do |comment| %> + + <%= comment.author %> + <%= markdown_to_html(comment.text) %> + + <%= link_to 'Show', comment %> + <%= link_to 'Edit', edit_comment_path(comment) %> + <%= button_to 'Destroy', comment, method: :delete, data: { turbo_confirm: 'Are you sure?' }, class: "text-red-500 cursor-pointer" %> + + + <% end %> + + -<%= link_to 'New Comment', new_comment_path %> + <%= link_to 'New Comment', new_comment_path, class: "not-prose px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800" %> +
diff --git a/app/views/comments/new.html.erb b/app/views/comments/new.html.erb index 17b22f25..bde1228d 100644 --- a/app/views/comments/new.html.erb +++ b/app/views/comments/new.html.erb @@ -1,5 +1,5 @@ -

New Comment

+

New Comment

-<%= render 'form' %> +<%= render 'form', comment: @comment %> <%= link_to 'Back', comments_path %> diff --git a/app/views/comments/show.html.erb b/app/views/comments/show.html.erb index 2e77ff5e..074a043a 100644 --- a/app/views/comments/show.html.erb +++ b/app/views/comments/show.html.erb @@ -1,5 +1,7 @@ -
-

<%= notice %>

+
+ <% if notice %> +

<%= notice %>

+ <% end %>

Author: diff --git a/app/views/comments/stimulus.html.erb b/app/views/comments/stimulus.html.erb new file mode 100644 index 00000000..087bf05d --- /dev/null +++ b/app/views/comments/stimulus.html.erb @@ -0,0 +1,20 @@ +

Stimulus + Rails Backend (with react_on_rails gem)

+ +<%= render "pages/header" %> + +
+

Comments

+ +
    +
  • Force Refresh of All Comments.
  • +
  • Text supports Github Flavored Markdown.
  • +
  • Comments older than 24 hours are deleted.
  • +
  • Name is preserved. Text is reset, between submits
  • +
  • To see Action Cable instantly update two browsers, open two browsers and submit a comment!
  • +
+ +
+ <%= render "comments/turbo/horizontal_form" %> + <%= render "comments/turbo/comment_list" %> +
+
diff --git a/app/views/comments/turbo/_comment_list.html.erb b/app/views/comments/turbo/_comment_list.html.erb new file mode 100644 index 00000000..77b752f9 --- /dev/null +++ b/app/views/comments/turbo/_comment_list.html.erb @@ -0,0 +1,12 @@ +<%= turbo_frame_tag "comment_list", class: "comment_list", data: { turbo: true } do %> + + Hidden Refresh Button +
+ <% @comments.each do |comment| %> +
+

<%= comment.author %>

+ <%= markdown_to_html(comment.text) %> +
+ <% end %> +
+<% end %> diff --git a/app/views/comments/turbo/_error_notice.html.erb b/app/views/comments/turbo/_error_notice.html.erb new file mode 100644 index 00000000..4fe5c3c9 --- /dev/null +++ b/app/views/comments/turbo/_error_notice.html.erb @@ -0,0 +1,5 @@ + diff --git a/app/views/comments/turbo/_horizontal_form.html.erb b/app/views/comments/turbo/_horizontal_form.html.erb new file mode 100644 index 00000000..e1f6f91d --- /dev/null +++ b/app/views/comments/turbo/_horizontal_form.html.erb @@ -0,0 +1,23 @@ +<%= turbo_frame_tag "form_tabs", data: { turbo: true } do %> + <%= render "comments/turbo/error_notice" %> + <%= render "comments/turbo/tabs" %> + +
+ + <%= form_with(model: @comment, data: { action: "turbo:submit-end->comments#resetText" }, class: "form-horizontal flex flex-col gap-4") do |f| %> +
+ <%= f.label "Name", class: "w-full lg:w-2/12 lg:text-end shrink-0" %> + <%= f.text_field :author, data: { comments_target: "commentAuthor" }, class: "px-3 py-1 leading-4 border border-gray-300 rounded w-full", placeholder: "Your Name" %> +
+ +
+ <%= f.label :text, class: "w-full lg:w-2/12 lg:text-end shrink-0" %> + <%= f.text_field :text, data: { comments_target: "commentText" }, class: "px-3 py-1 leading-4 border border-gray-300 rounded w-full", placeholder: "Say something using markdown..." %> +
+ +
+ + <%= f.submit "Post", class: "self-start px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800" %> +
+ <% end %> +<% end %> diff --git a/app/views/comments/turbo/_inline_form.html.erb b/app/views/comments/turbo/_inline_form.html.erb new file mode 100644 index 00000000..b25a0a40 --- /dev/null +++ b/app/views/comments/turbo/_inline_form.html.erb @@ -0,0 +1,22 @@ +<%= turbo_frame_tag "form_tabs", data: { turbo: true } do %> + <%= render "comments/turbo/error_notice" %> + <%= render "comments/turbo/tabs" %> + +
+ + <%= form_with(model: @comment, data: { action: "turbo:submit-end->comments#resetText" }, class: "form-inline flex flex-col lg:flex-row flex-wrap gap-4") do |f| %> +
+ <%= f.label "Name", class: "form-label mr-15" %> + <%= f.text_field :author, data: { comments_target: "commentAuthor" }, class: "px-3 py-1 leading-4 border border-gray-300 rounded", placeholder: "Your Name" %> +
+ +
+ <%= f.label :text, class: "form-label mr-15" %> + <%= f.text_field :text, data: { comments_target: "commentText" }, class: "px-3 py-1 leading-4 border border-gray-300 rounded", placeholder: "Say something using markdown..." %> +
+ +
+ <%= f.submit "Post", class: "self-start px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800" %> +
+ <% end %> +<% end %> diff --git a/app/views/comments/turbo/_stacked_form.html.erb b/app/views/comments/turbo/_stacked_form.html.erb new file mode 100644 index 00000000..095b012a --- /dev/null +++ b/app/views/comments/turbo/_stacked_form.html.erb @@ -0,0 +1,22 @@ +<%= turbo_frame_tag "form_tabs", data: { turbo: true } do %> + <%= render "comments/turbo/error_notice" %> + <%= render "comments/turbo/tabs" %> + +
+ + <%= form_with(model: @comment, data: { action: "turbo:submit-end->comments#resetText" }, class: "flex flex-col gap-4") do |f| %> +
+ <%= f.label "Name", class: "w-full" %> + <%= f.text_field :author, data: { comments_target: "commentAuthor" }, class: "px-3 py-1 leading-4 border border-gray-300 rounded w-full", placeholder: "Your Name" %> +
+ +
+ <%= f.label :text, class: "w-full" %> + <%= f.text_field :text, data: { comments_target: "commentText" }, class: "px-3 py-1 leading-4 border border-gray-300 rounded w-full", placeholder: "Say something using markdown..." %> +
+ +
+ <%= f.submit "Post", class: "self-start px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800" %> +
+ <% end %> +<% end %> diff --git a/app/views/comments/turbo/_tabs.html.erb b/app/views/comments/turbo/_tabs.html.erb new file mode 100644 index 00000000..18e19413 --- /dev/null +++ b/app/views/comments/turbo/_tabs.html.erb @@ -0,0 +1,5 @@ +
+ <%= link_to "Horizontal Form", horizontal_form_path, class: "px-6 py-2 font-semibold border-0 rounded #{@form_type == "horizontal" ? "text-sky-50 bg-sky-600" : "text-sky-600 hover:bg-gray-100"}" %> + <%= link_to "Stacked Form", stacked_form_path, class: "px-6 py-2 font-semibold border-0 rounded #{@form_type == "stacked" ? "text-sky-50 bg-sky-600" : "text-sky-600 hover:bg-gray-100" }" %> + <%= link_to "Inline Form", inline_form_path, class: "px-6 py-2 font-semibold border-0 rounded #{@form_type == "inline" ? "text-sky-50 bg-sky-600" : "text-sky-600 hover:bg-gray-100"}" %> +
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index df9c4222..45dbfa27 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,31 +1,31 @@ + + RailsReactTutorial - <%= stylesheet_pack_tag('vendor-bundle', - media: 'all', - 'data-turbolinks-track': true) %> + <%= stylesheet_pack_tag('client-bundle', + media: 'all', + 'data-turbolinks-track': true) %> - <%= stylesheet_pack_tag('app-bundle', - media: 'all', - 'data-turbolinks-track': true) %> - - <%= javascript_pack_tag('vendor-bundle', 'data-turbolinks-track': true) %> - <%= javascript_pack_tag('app-bundle', 'data-turbolinks-track': true) %> + <%= javascript_pack_tag('client-bundle', + 'data-turbolinks-track': true, + defer: true) %> <%= csrf_meta_tags %> - + + <%= react_component "NavigationBarApp" %> -<%= react_component "NavigationBarApp" %> -
- <%= yield %> -
+
+ <%= yield %> +
- -<%= redux_store_hydration_data %> + <%= react_component "Footer" %> + + <%= redux_store_hydration_data %> diff --git a/app/views/layouts/stimulus_layout.html.erb b/app/views/layouts/stimulus_layout.html.erb new file mode 100644 index 00000000..92bf77d8 --- /dev/null +++ b/app/views/layouts/stimulus_layout.html.erb @@ -0,0 +1,27 @@ + + + + + + RailsReactTutorial + + <%= stylesheet_pack_tag('client-bundle', + media: 'all', + 'data-turbolinks-track': true) %> + + <%= javascript_pack_tag('stimulus-bundle', + 'data-turbolinks-track': true, + defer: true) %> + + <%= csrf_meta_tags %> + + + <%= react_component "NavigationBarApp" %> + +
+ <%= yield %> +
+ + <%= react_component "Footer" %> + + diff --git a/app/views/pages/_header.html.erb b/app/views/pages/_header.html.erb index 85b3e10c..6f575f7a 100644 --- a/app/views/pages/_header.html.erb +++ b/app/views/pages/_header.html.erb @@ -1,40 +1,47 @@ -

Current Commit: - <%= link_to git_commit_sha_short, - "https://github.com/shakacode/react-webpack-rails-tutorial/commit/#{git_commit_sha}", - id: "git-commit-sha" %> -

-
    -
  • - <%= link_to "Can ShakaCode Help You?", - "https://blog.shakacode.com/can-shakacode-help-you-4a5b1e5a8a63#.jex6tg9w9" %> - We're actively seeking new projects with React, React-Native, or Ruby on Rails. -
  • +
    +

    Current Commit: + <%= link_to git_commit_sha_short, + "https://github.com/shakacode/react-webpack-rails-tutorial/commit/#{git_commit_sha}", + id: "git-commit-sha" %> +

    -
  • - See the - <%= link_to "github.com/shakacode/react-webpack-rails-tutorial/README.md", - "https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/README.md" %> - for details of how this example site was built. -
  • -
  • - Read <%= link_to "Documentation for React on Rails", - "https://shakacode.gitbooks.io/react-on-rails/content/" %> and - <%= link_to "The React on Rails Doctrine", - "http://www.shakacode.com/2016/01/27/the-react-on-rails-doctrine.html" %>. -
  • -
  • - See our React Native Client: - <%= link_to "shakacode/react-native-tutorial", - "https://github.com/shakacode/react-native-tutorial" %>. -
  • -
  • - Watch the <%= link_to "React On Rails Tutorial Series", - "https://www.youtube.com/playlist?list=PL5VAKH-U1M6dj84BApfUtvBjvF-0-JfEU" %>. -
  • -
  • - <%= link_to "ShakaCode", "http://www.shakacode.com"%> - is doing support for React on Rails, including a private Slack channel, source code reviews, and pair programming sessions. - <%= link_to "Click here", "http://www.shakacode.com/work/index.html" %> for more information. -
  • -
-
+
    +
  • + <%= link_to "Can ShakaCode Help You?", + "https://blog.shakacode.com/can-shakacode-help-you-4a5b1e5a8a63#.jex6tg9w9" %> + We're actively seeking new projects with React, React-Native, or Ruby on Rails +
  • +
  • + This project is deployed on + <%= link_to "Control Plane", + "https://shakacode.controlplane.com" %> + using + <%= link_to "Heroku to Control Plane", + "https://github.com/shakacode/heroku-to-control-plane" %> + Ruby gem. +
  • +
  • + See the + <%= link_to "github.com/shakacode/react-webpack-rails-tutorial/README.md", + "https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/README.md" %> + for details of how this example site was built. +
  • +
  • + Read <%= link_to "Documentation for React on Rails", + "https://shakacode.gitbooks.io/react-on-rails/content/" %> and + <%= link_to "The React on Rails Doctrine", + "https://www.shakacode.com/blog/the-react-on-rails-doctrine" %>. +
  • +
  • + Watch the <%= link_to "React On Rails Tutorial Series", + "https://www.youtube.com/playlist?list=PL5VAKH-U1M6dj84BApfUtvBjvF-0-JfEU" %>. +
  • +
  • + <%= link_to "ShakaCode", "http://www.shakacode.com"%> + is doing support for React on Rails, including a private Slack channel, source code reviews, and pair programming sessions. + <%= link_to "Click here", "http://www.shakacode.com/work/index.html" %> for more information. +
  • +
+
+ +
diff --git a/app/views/pages/index.html.erb b/app/views/pages/index.html.erb index bbd3fe2a..b76f6f67 100644 --- a/app/views/pages/index.html.erb +++ b/app/views/pages/index.html.erb @@ -1,9 +1,9 @@

<%= link_to "Open Source example", "https://github.com/shakacode/react-webpack-rails-tutorial/" %> of using the <%= link_to "React on Rails gem", "https://github.com/shakacode/react_on_rails" %> + and the <%= link_to "Shakapacker gem", "https://github.com/shakacode/shakapacker" %>

Using <%= link_to "Ruby on Rails", "http://rubyonrails.org/" %> with - <%= link_to "webpacker_lite", "https://github.com/shakacode/webpacker_lite" %> + <%= link_to "Action Cable", "http://guides.rubyonrails.org/action_cable_overview.html" %> + <%= link_to "React", "http://facebook.github.io/react/" %> (Server rendering) + <%= link_to "Redux", "https://github.com/reactjs/redux" %> + diff --git a/app/views/pages/rescript.html.erb b/app/views/pages/rescript.html.erb new file mode 100644 index 00000000..8afe2d40 --- /dev/null +++ b/app/views/pages/rescript.html.erb @@ -0,0 +1 @@ +<%= react_component "RescriptShow", prerender: true %> diff --git a/app/views/pages/simple.html.erb b/app/views/pages/simple.html.erb index 704c70d6..774948b5 100644 --- a/app/views/pages/simple.html.erb +++ b/app/views/pages/simple.html.erb @@ -1,13 +1,16 @@

Using React (no Flux framework) + Rails Backend (with react_on_rails gem)

+

This example is much simpler than the one using React + Redux and is appropriate when:

+
  • No or minimal MVC
  • No async necessary
  • No server rendering
  • No pre-population of props
-
+ +
<%= react_component('SimpleCommentScreen', props: {}, prerender: false) %> diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..6e2c3800 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,32 @@ +module.exports = function (api) { + const defaultConfigFunc = require('shakapacker/package/babel/preset.js'); + const resultConfig = defaultConfigFunc(api); + const isProductionEnv = api.env('production'); + + const changesOnDefault = { + presets: [ + [ + '@babel/preset-react', + { + runtime: 'automatic', + development: !isProductionEnv, + useBuiltIns: true, + }, + ], + ].filter(Boolean), + plugins: [ + process.env.WEBPACK_SERVE && 'react-refresh/babel', + isProductionEnv && [ + 'babel-plugin-transform-react-remove-prop-types', + { + removeImport: true, + }, + ], + ].filter(Boolean), + }; + + resultConfig.presets = [...resultConfig.presets, ...changesOnDefault.presets]; + resultConfig.plugins = [...resultConfig.plugins, ...changesOnDefault.plugins]; + + return resultConfig; +}; diff --git a/bin/dev b/bin/dev new file mode 100755 index 00000000..bc3f590e --- /dev/null +++ b/bin/dev @@ -0,0 +1,30 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +def installed?(process) + IO.popen "#{process} -v" +rescue Errno::ENOENT + false +end + +def run(process) + system "#{process} start -f Procfile.dev" +rescue Errno::ENOENT + warn <<~MSG + ERROR: + Please ensure `Procfile.dev` exists in your project! + MSG + exit! +end + +if installed? "overmind" + run "overmind" +elsif installed? "foreman" + run "foreman" +else + warn <<~MSG + NOTICE: + For this script to run, you need either 'overmind' or 'foreman' installed on your machine. Please try this script after installing one of them. + MSG + exit! +end diff --git a/bin/dev-static b/bin/dev-static new file mode 100755 index 00000000..d0d255c6 --- /dev/null +++ b/bin/dev-static @@ -0,0 +1,30 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +def installed?(process) + IO.popen "#{process} -v" +rescue Errno::ENOENT + false +end + +def run(process) + system "#{process} start -f Procfile.dev-static" +rescue Errno::ENOENT + warn <<~MSG + ERROR: + Please ensure `Procfile.dev-static` exists in your project! + MSG + exit! +end + +if installed? "overmind" + run "overmind" +elsif installed? "foreman" + run "foreman" +else + warn <<~MSG + NOTICE: + For this script to run, you need either 'overmind' or 'foreman' installed on your machine. Please try this script after installing one of them. + MSG + exit! +end diff --git a/bin/rails b/bin/rails index 5191e692..efc03774 100755 --- a/bin/rails +++ b/bin/rails @@ -1,4 +1,4 @@ #!/usr/bin/env ruby -APP_PATH = File.expand_path('../../config/application', __FILE__) -require_relative '../config/boot' -require 'rails/commands' +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake index 17240489..4fbf10b9 100755 --- a/bin/rake +++ b/bin/rake @@ -1,4 +1,4 @@ #!/usr/bin/env ruby -require_relative '../config/boot' -require 'rake' +require_relative "../config/boot" +require "rake" Rake.application.run diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 00000000..40330c0f --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# explicit rubocop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup index 589bee82..8dfadbb5 100755 --- a/bin/setup +++ b/bin/setup @@ -1,34 +1,38 @@ #!/usr/bin/env ruby -require 'pathname' -require 'fileutils' -include FileUtils +require "fileutils" -# path to your application root. -APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +APP_ROOT = File.expand_path("..", __dir__) def system!(*args) - system(*args) || abort("\n== Command #{args} failed ==") + system(*args, exception: true) end -chdir APP_ROOT do - # This script is a starting point to setup your application. +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. - puts '== Installing dependencies ==' - system! 'gem install bundler --conservative' - system('bundle check') or system!('bundle install') + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") + + # Install JavaScript dependencies + system! "bin/yarn" # puts "\n== Copying sample files ==" - # unless File.exist?('config/database.yml') - # cp 'config/database.yml.sample', 'config/database.yml' + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" # end puts "\n== Preparing database ==" - system! 'bin/rails db:setup' + system! "bin/rails db:prepare" puts "\n== Removing old logs and tempfiles ==" - system! 'bin/rails log:clear tmp:clear' + system! "bin/rails log:clear tmp:clear" - puts "\n== Restarting application server ==" - system! 'bin/rails restart' + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end end diff --git a/bin/shakapacker b/bin/shakapacker new file mode 100755 index 00000000..13a008dc --- /dev/null +++ b/bin/shakapacker @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= "development" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__) + +require "bundler/setup" +require "shakapacker" +require "shakapacker/webpack_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Shakapacker::WebpackRunner.run(ARGV) +end diff --git a/bin/shakapacker-dev-server b/bin/shakapacker-dev-server new file mode 100755 index 00000000..5ae88979 --- /dev/null +++ b/bin/shakapacker-dev-server @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= "development" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__) + +require "bundler/setup" +require "shakapacker" +require "shakapacker/dev_server_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Shakapacker::DevServerRunner.run(ARGV) +end diff --git a/bin/spring b/bin/spring deleted file mode 100755 index 7fe232c3..00000000 --- a/bin/spring +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env ruby - -# This file loads spring without using Bundler, in order to be fast. -# It gets overwritten when you run the `spring binstub` command. - -unless defined?(Spring) - require 'rubygems' - require 'bundler' - - if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) - Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } - gem 'spring', match[1] - require 'spring/binstub' - end -end diff --git a/bin/thrust b/bin/thrust new file mode 100755 index 00000000..36bde2d8 --- /dev/null +++ b/bin/thrust @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/bin/yarn b/bin/yarn new file mode 100755 index 00000000..fe733862 --- /dev/null +++ b/bin/yarn @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + yarn = ENV["PATH"].split(File::PATH_SEPARATOR). + select { |dir| File.expand_path(dir) != __dir__ }. + product(["yarn", "yarnpkg", "yarn.cmd", "yarn.ps1"]). + map { |dir, file| File.expand_path(file, dir) }. + find { |file| File.executable?(file) } + + if yarn + exec yarn, *ARGV + else + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/bsconfig.json b/bsconfig.json new file mode 100644 index 00000000..1f842dce --- /dev/null +++ b/bsconfig.json @@ -0,0 +1,28 @@ +{ + "name": "react-webpack-rails-tutorial", + "sources": [ + { + "dir": "client/app/bundles/comments/rescript", + "subdirs": true + } + ], + "package-specs": [ + { + "module": "es6", + "in-source": true + } + ], + "bsc-flags": ["-open JsonCombinators", "-open Belt"], + "suffix": ".bs.js", + "bs-dependencies": [ + "@rescript/react", + "@rescript/core", + "@glennsl/rescript-fetch", + "@glennsl/rescript-json-combinators", + "rescript-react-on-rails" + ], + "jsx": { + "version": 4, + "mode": "automatic" + } +} diff --git a/client/.babelrc b/client/.babelrc deleted file mode 100644 index e68d2fea..00000000 --- a/client/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015", "stage-2", "react"] -} diff --git a/client/.bootstraprc b/client/.bootstraprc deleted file mode 100644 index 5286126b..00000000 --- a/client/.bootstraprc +++ /dev/null @@ -1,123 +0,0 @@ ---- -# Output debugging info -# loglevel: debug - -# Major version of Bootstrap: 3 or 4 -bootstrapVersion: 3 - -# Webpack loaders, order matters -styleLoaders: - - style - - css - - sass - -# Extract styles to stand-alone css file -# Different settings for different environments can be used, -# It depends on value of NODE_ENV environment variable -# This param can also be set in webpack config: -# entry: 'bootstrap-loader/extractStyles' -extractStyles: false -# env: -# development: -# extractStyles: false -# production: -# extractStyles: true - -# Customize Bootstrap variables that get imported before the original Bootstrap variables. -# Thus original Bootstrap variables can depend on values from here. All the bootstrap -# variables are configured with !default, and thus, if you define the variable here, then -# that value is used, rather than the default. However, many bootstrap variables are derived -# from other bootstrap variables, and thus, you want to set this up before we load the -# official bootstrap versions. -# For example, _variables.scss contains: -# $input-color: $gray !default; -# This means you can define $input-color before we load _variables.scss -preBootstrapCustomizations: ./app/assets/styles/bootstrap-pre-customizations.scss - -# This gets loaded after bootstrap/variables is loaded and before bootstrap is loaded. -# A good example of this is when you want to override a bootstrap variable to be based -# on the default value of bootstrap. This is pretty specialized case. Thus, you normally -# just override bootrap variables in preBootstrapCustomizations so that derived -# variables will use your definition. -# -# For example, in _variables.scss: -# $input-height: (($font-size-base * $line-height) + ($input-padding-y * 2) + ($border-width * 2)) !default; -# This means that you could define this yourself in preBootstrapCustomizations. Or you can do -# this in bootstrapCustomizations to make the input height 10% bigger than the default calculation. -# Thus you can leverage the default calculations. -# $input-height: $input-height * 1.10; -# bootstrapCustomizations: ./app/assets/styles/bootstrap-customizations.scss - -# Import your custom styles here. You have access to all the bootstrap variables. If you require -# your sass files separately, you will not have access to the bootstrap variables, mixins, clases, etc. -# Usually this endpoint-file contains list of @imports of your application styles. -# But since we use CSS Modules, we don't need it - every module gets imported from JS component. -# appStyles: ./app/styles/app.scss - -### Bootstrap styles -styles: - - # Mixins - mixins: true - - # Reset and dependencies - normalize: true - print: true - glyphicons: true - - # Core CSS - scaffolding: true - type: true - code: true - grid: true - tables: true - forms: true - buttons: true - - # Components - component-animations: true - dropdowns: true - button-groups: true - input-groups: true - navs: true - navbar: true - breadcrumbs: true - pagination: true - pager: true - labels: true - badges: true - jumbotron: true - thumbnails: true - alerts: true - progress-bars: true - media: true - list-group: true - panels: true - wells: true - responsive-embed: true - close: true - - # Components w/ JavaScript - modals: true - tooltip: true - popovers: true - carousel: true - - # Utility classes - utilities: true - responsive-utilities: true - -### Bootstrap scripts -scripts: - transition: true - alert: true - button: true - carousel: true - collapse: true - dropdown: true - modal: true - tooltip: true - popover: true - scrollspy: true - tab: true - affix: true diff --git a/client/.eslintignore b/client/.eslintignore deleted file mode 100644 index 369ecb07..00000000 --- a/client/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules - -app/libs/i18n/translations.js -app/libs/i18n/default.js diff --git a/client/.eslintrc.yml b/client/.eslintrc.yml deleted file mode 100644 index b76a4d79..00000000 --- a/client/.eslintrc.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -parser: babel-eslint - -extends: eslint-config-shakacode - -plugins: - - react - -globals: - __DEBUG_SERVER_ERRORS__: true - __SERVER_ERRORS__: true - -env: - browser: true - node: true - mocha: true - -rules: - import/extensions: [2, { js: "never", jsx: "never" }] - - # Good idea, but let's go to flow before fixing all these - react/forbid-prop-types: 0 - -# arrow-parens: -# - 0 -# - "as-needed" - -settings: - import/resolver: - webpack: - config: 'webpack.client.base.config.js' diff --git a/client/README.md b/client/README.md index fd6171fd..802c2399 100644 --- a/client/README.md +++ b/client/README.md @@ -26,11 +26,7 @@ Soon to be in gulpfile....but gulp-eslint depends on eslint depends on So don't use `yarn run gulp lint` yet. -For now: - - bin/lint - -Or (from either top level or within `client` directory) +From either top level or within `client` directory yarn run lint diff --git a/client/app/bundles/comments/components/CommentScreen/images/railsonmaui.png b/client/app/assets/images/railsonmaui.png similarity index 100% rename from client/app/bundles/comments/components/CommentScreen/images/railsonmaui.png rename to client/app/assets/images/railsonmaui.png diff --git a/client/app/bundles/comments/components/CommentScreen/images/twitter_64.png b/client/app/assets/images/twitter_64.png similarity index 100% rename from client/app/bundles/comments/components/CommentScreen/images/twitter_64.png rename to client/app/assets/images/twitter_64.png diff --git a/client/app/assets/styles/app-variables.scss b/client/app/assets/styles/app-variables.scss index e3516f20..acd175de 100644 --- a/client/app/assets/styles/app-variables.scss +++ b/client/app/assets/styles/app-variables.scss @@ -1,10 +1,4 @@ -// Defining application SASS variables in stand-alone file, -// so we can use them in bootstrap-loader and CSS Modules via sass-resources-loader -$body-bg: #EFF8FB; // background w/ character -$navbar-default-bg: #FFFFE0; // fancy yellow navbar -$font-size-base: 15px; // make it bigger! -$font-family-sans-serif: 'OpenSans-Light'; // apply custom font +// $animation-duration must correspond to CSSTransition timeout value in: +// client/app/bundles/comments/components/CommentBox/CommentList.jsx -// It will be used in SASS components imported as CSS Modules -$comment-author-color: blue; -$comment-text-color: purple; +$animation-duration: 0.5s; diff --git a/client/app/assets/styles/application.css b/client/app/assets/styles/application.css new file mode 100644 index 00000000..f9e47b8a --- /dev/null +++ b/client/app/assets/styles/application.css @@ -0,0 +1,20 @@ +@font-face { + font-family: 'OpenSans-Light'; + src: url('../../assets/fonts/OpenSans-Light.ttf') format('truetype'); +} + +@import 'tailwindcss/base'; +@import 'tailwindcss/components'; +@import 'tailwindcss/utilities'; + +h2 { + @apply text-3xl font-medium mt-5 mb-2.5; +} + +h3 { + @apply text-xl font-medium mt-5 mb-2.5 +} + +a { + @apply text-sky-700 +} diff --git a/client/app/assets/styles/bootstrap-pre-customizations.scss b/client/app/assets/styles/bootstrap-pre-customizations.scss deleted file mode 100644 index 2003b430..00000000 --- a/client/app/assets/styles/bootstrap-pre-customizations.scss +++ /dev/null @@ -1,10 +0,0 @@ -// These variables get loaded BEFORE Bootstrap thus overriding them in Bootstrap. -@import './app-variables'; - -// This path is relative to this file! -$fonts-url-path: '../fonts'; - -@font-face { - font-family: 'OpenSans-Light'; - src: url('#{$fonts-url-path}/OpenSans-Light.ttf') format('truetype'); -} diff --git a/client/app/assets/styles/stimulus.scss b/client/app/assets/styles/stimulus.scss new file mode 100644 index 00000000..e69de29b diff --git a/client/app/bundles/comments/actions/commentsActionCreators.js b/client/app/bundles/comments/actions/commentsActionCreators.js index 418a8675..d64853cb 100644 --- a/client/app/bundles/comments/actions/commentsActionCreators.js +++ b/client/app/bundles/comments/actions/commentsActionCreators.js @@ -1,4 +1,4 @@ -import requestsManager from 'libs/requestsManager'; +import requestsManager from '../../../libs/requestsManager'; import * as actionTypes from '../constants/commentsConstants'; export function setIsFetching() { @@ -51,24 +51,20 @@ export function submitCommentFailure(error) { export function fetchComments() { return (dispatch) => { dispatch(setIsFetching()); - return ( - requestsManager - .fetchEntities() - .then(res => dispatch(fetchCommentsSuccess(res.data))) - .catch(error => dispatch(fetchCommentsFailure(error))) - ); + return requestsManager + .fetchEntities() + .then((res) => dispatch(fetchCommentsSuccess(res.data))) + .catch((error) => dispatch(fetchCommentsFailure(error))); }; } export function submitComment(comment) { return (dispatch) => { dispatch(setIsSaving()); - return ( - requestsManager - .submitEntity({ comment }) - .then(res => dispatch(submitCommentSuccess(res.data))) - .catch(error => dispatch(submitCommentFailure(error))) - ); + return requestsManager + .submitEntity({ comment }) + .then((res) => dispatch(submitCommentSuccess(res.data))) + .catch((error) => dispatch(submitCommentFailure(error))); }; } diff --git a/client/app/bundles/comments/components/CommentBox/CommentBox.jsx b/client/app/bundles/comments/components/CommentBox/CommentBox.jsx index 68611457..3376f74a 100644 --- a/client/app/bundles/comments/components/CommentBox/CommentBox.jsx +++ b/client/app/bundles/comments/components/CommentBox/CommentBox.jsx @@ -1,36 +1,34 @@ -import React, { PropTypes } from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; import Immutable from 'immutable'; -import ActionCable from 'actioncable'; import _ from 'lodash'; +import { injectIntl } from 'react-intl'; import BaseComponent from 'libs/components/BaseComponent'; -import { injectIntl, intlShape } from 'react-intl'; import SelectLanguage from 'libs/i18n/selectLanguage'; import { defaultMessages, defaultLocale } from 'libs/i18n/default'; - import CommentForm from './CommentForm/CommentForm'; -import CommentList, { CommentPropTypes } from './CommentList/CommentList'; -import css from './CommentBox.scss'; +import CommentList, { commentPropTypes } from './CommentList/CommentList'; +import css from './CommentBox.module.scss'; class CommentBox extends BaseComponent { static propTypes = { pollInterval: PropTypes.number.isRequired, actions: PropTypes.shape({ - fetchComments: React.PropTypes.function, + fetchComments: PropTypes.func, }), data: PropTypes.shape({ - isFetching: React.PropTypes.boolean, - isSaving: React.PropTypes.boolean, - submitCommentError: React.PropTypes.string, - $$comments: React.PropTypes.arrayOf(CommentPropTypes), + isFetching: PropTypes.func, + isSaving: PropTypes.bool, + submitCommentError: PropTypes.string, + $$comments: PropTypes.arrayOf(commentPropTypes), }).isRequired, - intl: intlShape.isRequired, + // eslint-disable-next-line react/forbid-prop-types + intl: PropTypes.objectOf(PropTypes.any).isRequired, }; constructor() { super(); - _.bindAll(this, [ - 'refreshComments', - ]); + _.bindAll(this, ['refreshComments']); this.cable = null; } @@ -38,20 +36,25 @@ class CommentBox extends BaseComponent { const { messageReceived } = this.props.actions; const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; const cableUrl = `${protocol}${window.location.hostname}:${window.location.port}/cable`; + // ActionCable is a global added through webpack.providePlugin + // eslint-disable-next-line no-undef this.cable = ActionCable.createConsumer(cableUrl); /* eslint no-console: ["error", { allow: ["log"] }] */ - this.cable.subscriptions.create({ channel: 'CommentsChannel' }, { - connected: () => { - console.log('connected'); - }, - disconnected: () => { - console.log('disconnected'); - }, - received: (comment) => { - messageReceived(Immutable.fromJS(comment)); + this.cable.subscriptions.create( + { channel: 'CommentsChannel' }, + { + connected: () => { + console.log('connected'); + }, + disconnected: () => { + console.log('disconnected'); + }, + received: (comment) => { + messageReceived(Immutable.fromJS(comment)); + }, }, - }); + ); } componentDidMount() { @@ -75,26 +78,29 @@ class CommentBox extends BaseComponent { const cssTransitionGroupClassNames = { enter: css.elementEnter, enterActive: css.elementEnterActive, - leave: css.elementLeave, - leaveActive: css.elementLeaveActive, + exit: css.elementLeave, + exitActive: css.elementLeaveActive, }; const locale = data.get('locale') || defaultLocale; - /* eslint-disable no-script-url */ return ( -
+

{formatMessage(defaultMessages.comments)} {data.get('isFetching') && formatMessage(defaultMessages.loading)}

- { SelectLanguage(actions.setLocale, locale) } + {SelectLanguage(actions.setLocale, locale)} diff --git a/client/app/bundles/comments/components/CommentBox/CommentBox.module.scss b/client/app/bundles/comments/components/CommentBox/CommentBox.module.scss new file mode 100644 index 00000000..60cb622d --- /dev/null +++ b/client/app/bundles/comments/components/CommentBox/CommentBox.module.scss @@ -0,0 +1,19 @@ +// $animation-duration is set in client/app/assets/styles/app-variables.scss + +.elementEnter { + opacity: 0.01; + + &.elementEnterActive { + opacity: 1; + transition: opacity $animation-duration ease-in; + } +} + +.elementLeave { + opacity: 1; + + &.elementLeaveActive { + opacity: 0.01; + transition: opacity $animation-duration ease-in; + } +} diff --git a/client/app/bundles/comments/components/CommentBox/CommentForm/CommentForm.jsx b/client/app/bundles/comments/components/CommentBox/CommentForm/CommentForm.jsx index 5b49b3de..ea81d06e 100644 --- a/client/app/bundles/comments/components/CommentBox/CommentForm/CommentForm.jsx +++ b/client/app/bundles/comments/components/CommentBox/CommentForm/CommentForm.jsx @@ -1,43 +1,22 @@ -// NOTE: https://github.com/react-bootstrap/react-bootstrap/issues/1850 seesm to require string -// refs and not the callback kind. /* eslint-disable react/no-find-dom-node, react/no-string-refs */ -import React, { PropTypes } from 'react'; -import ReactDOM from 'react-dom'; -import Col from 'react-bootstrap/lib/Col'; -import FormControl from 'react-bootstrap/lib/FormControl'; -import ControlLabel from 'react-bootstrap/lib/ControlLabel'; -import Form from 'react-bootstrap/lib/Form'; -import FormGroup from 'react-bootstrap/lib/FormGroup'; -import Button from 'react-bootstrap/lib/Button'; -import Nav from 'react-bootstrap/lib/Nav'; -import NavItem from 'react-bootstrap/lib/NavItem'; -import Alert from 'react-bootstrap/lib/Alert'; -import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; import _ from 'lodash'; -import { injectIntl, intlShape } from 'react-intl'; +import { injectIntl } from 'react-intl'; import { defaultMessages } from 'libs/i18n/default'; import BaseComponent from 'libs/components/BaseComponent'; -import css from './CommentForm.scss'; - const emptyComment = { author: '', text: '' }; -function bsStyleFor(propName, error) { - if (error) { - const errorData = (error && error.response && error.response.data) || {}; - return (propName in errorData) ? 'error' : 'success'; - } - - return null; -} - class CommentForm extends BaseComponent { static propTypes = { isSaving: PropTypes.bool.isRequired, - actions: PropTypes.object.isRequired, - error: PropTypes.any, - cssTransitionGroupClassNames: PropTypes.object.isRequired, - intl: intlShape.isRequired, + actions: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.any])).isRequired, + error: PropTypes.oneOfType([PropTypes.any]), + cssTransitionGroupClassNames: PropTypes.oneOfType([PropTypes.func, PropTypes.any]).isRequired, + // eslint-disable-next-line react/forbid-prop-types + intl: PropTypes.objectOf(PropTypes.any).isRequired, }; constructor(props, context) { @@ -47,12 +26,14 @@ class CommentForm extends BaseComponent { comment: emptyComment, }; - _.bindAll(this, [ - 'handleSelect', - 'handleChange', - 'handleSubmit', - 'resetAndFocus', - ]); + this.horizontalAuthorRef = React.createRef(); + this.horizontalTextRef = React.createRef(); + this.stackedAuthorRef = React.createRef(); + this.stackedTextRef = React.createRef(); + this.inlineAuthorRef = React.createRef(); + this.inlineTextRef = React.createRef(); + + _.bindAll(this, ['handleSelect', 'handleChange', 'handleSubmit', 'resetAndFocus']); } handleSelect(selectedKey) { @@ -65,22 +46,20 @@ class CommentForm extends BaseComponent { switch (this.state.formMode) { case 0: comment = { - author: ReactDOM.findDOMNode(this.refs.horizontalAuthorNode).value, - text: ReactDOM.findDOMNode(this.refs.horizontalTextNode).value, + author: this.horizontalAuthorRef.current.value, + text: this.horizontalTextRef.current.value, }; break; case 1: comment = { - author: ReactDOM.findDOMNode(this.refs.stackedAuthorNode).value, - text: ReactDOM.findDOMNode(this.refs.stackedTextNode).value, + author: this.stackedAuthorRef.current.value, + text: this.stackedTextRef.current.value, }; break; case 2: comment = { - // This is different because the input is a native HTML element - // rather than a React element. - author: ReactDOM.findDOMNode(this.refs.inlineAuthorNode).value, - text: ReactDOM.findDOMNode(this.refs.inlineTextNode).value, + author: this.inlineAuthorRef.current.value, + text: this.inlineTextRef.current.value, }; break; default: @@ -93,9 +72,7 @@ class CommentForm extends BaseComponent { handleSubmit(e) { e.preventDefault(); const { actions } = this.props; - actions - .submitComment(this.state.comment) - .then(this.resetAndFocus); + actions.submitComment(this.state.comment).then(this.resetAndFocus); } resetAndFocus() { @@ -108,13 +85,13 @@ class CommentForm extends BaseComponent { let ref; switch (this.state.formMode) { case 0: - ref = ReactDOM.findDOMNode(this.refs.horizontalTextNode); + ref = this.horizontalTextRef.current; break; case 1: - ref = ReactDOM.findDOMNode(this.refs.stackedTextNode); + ref = this.stackedTextRef.current; break; case 2: - ref = ReactDOM.findDOMNode(this.refs.inlineTextNode); + ref = this.inlineTextRef.current; break; default: throw new Error(`Unexpected state.formMode ${this.state.formMode}`); @@ -128,54 +105,52 @@ class CommentForm extends BaseComponent { return (

-
- - + +
+ + +
+ +
+ + +
+ +
+
+ +
+
); } @@ -185,45 +160,50 @@ class CommentForm extends BaseComponent { return (

-
- - {formatMessage(defaultMessages.inputNameLabel)} - +
+ + - - - {formatMessage(defaultMessages.inputTextLabel)} - + +
+ + - - - - + +
); @@ -235,71 +215,86 @@ class CommentForm extends BaseComponent { return (

-
- - - {formatMessage(defaultMessages.inputNameLabel)} - - +
+ + - - - - {formatMessage(defaultMessages.inputTextLabel)} - - + +
+ + - - - +
+ +
+ +
+
); } errorWarning() { - const error = this.props.error; + const { error, cssTransitionGroupClassNames } = this.props; // If there is no error, there is nothing to add to the DOM - if (!error) return null; + if (!error.error) return null; - const errorData = error.response && error.response.data; + const errorData = error.error.response && error.error.response.data; - const errorElements = _.transform(errorData, (result, errorText, errorFor) => { - result.push(
  • {_.upperFirst(errorFor)}: {errorText}
  • ); - }, []); + const errorElements = _.transform( + errorData, + (result, errorText, errorFor) => { + result.push( + +
  • + {_.upperFirst(errorFor)}: {errorText} +
  • +
    , + ); + }, + [], + ); return ( - +
    Your comment was not saved! -
      - {errorElements} -
    - +
      {errorElements}
    +
    ); } @@ -319,28 +314,46 @@ class CommentForm extends BaseComponent { throw new Error(`Unknown form mode: ${this.state.formMode}.`); } - const { cssTransitionGroupClassNames } = this.props; const { formatMessage } = this.props.intl; - // For animation with ReactCSSTransitionGroup - // https://facebook.github.io/react/docs/animation.html + // For animation with TransitionGroup + // https://reactcommunity.org/react-transition-group/transition-group // The 500 must correspond to the 0.5s in: - // client/app/bundles/comments/components/CommentBox/CommentBox.scss:6 + // client/app/bundles/comments/components/CommentBox/CommentBox.module.scss:6 return (
    - - {this.errorWarning()} - - - + {this.errorWarning()} + +
    + + + + {} +
    {inputForm}
    ); diff --git a/client/app/bundles/comments/components/CommentBox/CommentForm/CommentForm.scss b/client/app/bundles/comments/components/CommentBox/CommentForm/CommentForm.scss deleted file mode 100644 index 934d8f85..00000000 --- a/client/app/bundles/comments/components/CommentBox/CommentForm/CommentForm.scss +++ /dev/null @@ -1,14 +0,0 @@ -// scss-lint:disable ImportantRule -// Due to bootstrap overriding values in css-modules because of the specificity of the rule. -.nameFormControl { - margin-left: 10px; - margin-right: 20px; - // Must set width to !important b/c CSS modules value will be overriden by the bootstrap value - width: 150px !important; -} - -.textFormControl { - margin-left: 10px; - margin-right: 20px; - width: 250px !important; -} diff --git a/client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.jsx b/client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.jsx index 8f2d9fa1..e9ca50b5 100644 --- a/client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.jsx +++ b/client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.jsx @@ -1,30 +1,34 @@ -import BaseComponent from 'libs/components/BaseComponent'; -import React, { PropTypes } from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; -import marked from 'marked'; -import css from './Comment.scss'; +import { marked } from 'marked'; +import { gfmHeadingId } from 'marked-gfm-heading-id'; +import { mangle } from 'marked-mangle'; +import sanitizeHtml from 'sanitize-html'; -export default class Comment extends BaseComponent { - static propTypes = { - author: PropTypes.string.isRequired, - text: PropTypes.string.isRequired, - }; +marked.use(gfmHeadingId()); +marked.use(mangle()); - render() { - const { author, text } = this.props; - const rawMarkup = marked(text, { gfm: true, sanitize: true }); +const Comment = React.forwardRef((props, ref) => { + const { author, text } = props; + const rawMarkup = marked(text, { gfm: true }); + const sanitizedRawMarkup = sanitizeHtml(rawMarkup); - /* eslint-disable react/no-danger */ - return ( -
    -

    - {author} -

    - -
    - ); - } -} + /* eslint-disable react/no-danger */ + return ( +
    +

    {author}

    + +
    +
    + ); +}); + +Comment.displayName = 'Comment'; + +Comment.propTypes = { + author: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, +}; + +export default React.memo(Comment); diff --git a/client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.scss b/client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.scss deleted file mode 100644 index 9d156501..00000000 --- a/client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.scss +++ /dev/null @@ -1,11 +0,0 @@ -.comment { - - .commentAuthor { - color: $comment-author-color; // <--- Loaded via sass-resources-loader - } - - p { - color: $comment-text-color; // <-- This one also - } - -} diff --git a/client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.spec.jsx b/client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.spec.jsx index d3070d3a..ea8a7c3f 100644 --- a/client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.spec.jsx +++ b/client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.spec.jsx @@ -1,43 +1,32 @@ -import { React, expect, TestUtils } from 'libs/testHelper'; +import { React, TestUtils } from '../../../../../../libs/testHelper'; -import Comment from './Comment'; +import Comment from './Comment.jsx'; -const { - renderIntoDocument, - findRenderedDOMComponentWithClass, - findRenderedDOMComponentWithTag, -} = TestUtils; +const { renderIntoDocument, findRenderedDOMComponentWithClass, findRenderedDOMComponentWithTag } = TestUtils; describe('Comment', () => { it('renders an author and comment with proper css classes', () => { - const component = renderIntoDocument( - , - ); - - // TODO: Setup testing of CSS Modules classNames - // const comment = findRenderedDOMComponentWithTag(component, 'div'); - // expect(comment.className).to.equal('comment'); - // const author = findRenderedDOMComponentWithTag(component, 'h2'); - // expect(author.className).to.equal('comment-author'); + const component = renderIntoDocument(); + + const comment = findRenderedDOMComponentWithTag(component, 'div'); + expect(comment.className).toEqual('comment'); + const author = findRenderedDOMComponentWithTag(component, 'h2'); + expect(author.className).toEqual('commentAuthor js-comment-author'); const text = findRenderedDOMComponentWithTag(component, 'span'); - expect(text.className).to.equal('js-comment-text'); + expect(text.className).toEqual('js-comment-text'); }); it('shows the author', () => { - const component = renderIntoDocument( - , - ); + const component = renderIntoDocument(); const author = findRenderedDOMComponentWithClass(component, 'js-comment-author'); - expect(author.textContent).to.equal('Frank'); + expect(author.textContent).toEqual('Frank'); }); it('shows the comment text in markdown', () => { - const component = renderIntoDocument( - , - ); + const component = renderIntoDocument(); const comment = findRenderedDOMComponentWithClass(component, 'js-comment-text'); - expect(comment.textContent).to.equal('Hi!\n'); + expect(comment.textContent).toEqual('Hi!\n'); }); }); diff --git a/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.jsx b/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.jsx index afdc67fb..128f8878 100644 --- a/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.jsx +++ b/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.jsx @@ -1,16 +1,17 @@ -import Alert from 'react-bootstrap/lib/Alert'; -import BaseComponent from 'libs/components/BaseComponent'; import Immutable from 'immutable'; -import React, { PropTypes } from 'react'; -import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; import _ from 'lodash'; +import BaseComponent from '../../../../../libs/components/BaseComponent.jsx'; -import Comment from './Comment/Comment'; +import Comment from './Comment/Comment.jsx'; export const commentPropTypes = { $$comments: PropTypes.instanceOf(Immutable.List).isRequired, - error: PropTypes.any, - cssTransitionGroupClassNames: PropTypes.object.isRequired, + // TODO: Update error propType + error: PropTypes.string, + cssTransitionGroupClassNames: PropTypes.oneOfType([PropTypes.object]).isRequired, }; export default class CommentList extends BaseComponent { @@ -18,58 +19,68 @@ export default class CommentList extends BaseComponent { constructor(props, context) { super(props, context); - this.state = {}; + _.bindAll(this, 'errorWarning'); } errorWarning() { + const { error, cssTransitionGroupClassNames } = this.props; + // If there is no error, there is nothing to add to the DOM - if (!this.props.error) return null; + if (!error) return null; + + const nodeRef = React.createRef(null); + return ( - - Comments could not be retrieved. - A server error prevented loading comments. Please try again. - + +
    + Comments could not be retrieved. A server error prevented loading comments. Please + try again. +
    +
    ); } render() { const { $$comments, cssTransitionGroupClassNames } = this.props; - const commentNodes = $$comments.map(($$comment, index) => - - // `key` is a React-specific concept and is not mandatory for the - // purpose of this tutorial. if you're curious, see more here: - // http://facebook.github.io/react/docs/multiple-components.html#dynamic-children - , - ); + const commentNodes = $$comments.map(($$comment, index) => { + const nodeRef = React.createRef(null); + return ( + + + + ); + }); - // For animation with ReactCSSTransitionGroup - // https://facebook.github.io/react/docs/animation.html - // The 500 must correspond to the 0.5s in: - // client/app/bundles/comments/components/CommentBox/CommentBox.scss:6 + // For animation with TransitionGroup + // https://reactcommunity.org/react-transition-group/transition-group + // The 500 must correspond to $animation-duration in: + // client/app/assets/styles/app-variables.scss return (
    - - {this.errorWarning()} - + {this.errorWarning()} - + {commentNodes} - +
    ); } diff --git a/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.spec.jsx b/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.spec.jsx index 23318888..531f5728 100644 --- a/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.spec.jsx +++ b/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.spec.jsx @@ -1,20 +1,16 @@ -import { React, expect, TestUtils } from 'libs/testHelper'; import { List, Map } from 'immutable'; +import { React, TestUtils } from '../../../../../libs/testHelper'; -import CommentList from './CommentList'; -import Comment from './Comment/Comment'; +import CommentList from './CommentList.jsx'; +import Comment from './Comment/Comment.jsx'; -const { - renderIntoDocument, - findRenderedDOMComponentWithTag, - scryRenderedComponentsWithType, -} = TestUtils; +const { renderIntoDocument, findRenderedDOMComponentWithTag, scryRenderedComponentsWithType } = TestUtils; const cssTransitionGroupClassNames = { enter: 'elementEnter', enterActive: 'elementEnterActive', - leave: 'elementLeave', - leaveActive: 'elementLeaveActive', + exit: 'elementLeave', + exitActive: 'elementLeaveActive', }; describe('CommentList', () => { @@ -23,36 +19,36 @@ describe('CommentList', () => { id: 1, author: 'Frank', text: 'hi', + nodeRef: React.createRef(null), }), new Map({ id: 2, author: 'Furter', text: 'ho', + nodeRef: React.createRef(null), }), ); it('renders a list of Comments in normal order', () => { const component = renderIntoDocument( - , + , ); const list = scryRenderedComponentsWithType(component, Comment); - expect(list.length).to.equal(2); - expect(list[0].props.author).to.equal('Frank'); - expect(list[1].props.author).to.equal('Furter'); + expect(list.length).toEqual(2); + expect(list[0].props.author).toEqual('Frank'); + expect(list[1].props.author).toEqual('Furter'); }); it('renders an alert if errors', () => { const component = renderIntoDocument( , ); const alert = findRenderedDOMComponentWithTag(component, 'strong'); - expect(alert.textContent).to.equal('Comments could not be retrieved. '); + expect(alert.textContent).toEqual('Comments could not be retrieved. '); }); }); diff --git a/client/app/bundles/comments/components/CommentScreen/CommentScreen.jsx b/client/app/bundles/comments/components/CommentScreen/CommentScreen.jsx index 44e53e1f..909d3069 100644 --- a/client/app/bundles/comments/components/CommentScreen/CommentScreen.jsx +++ b/client/app/bundles/comments/components/CommentScreen/CommentScreen.jsx @@ -1,16 +1,15 @@ -import React, { PropTypes } from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; import BaseComponent from 'libs/components/BaseComponent'; import CommentBox from '../CommentBox/CommentBox'; -import css from './CommentScreen.scss'; export default class CommentScreen extends BaseComponent { - static propTypes = { - actions: PropTypes.object.isRequired, - data: PropTypes.object.isRequired, - locationState: PropTypes.object, + actions: PropTypes.oneOfType([PropTypes.object]).isRequired, + data: PropTypes.oneOfType([PropTypes.object]).isRequired, + locationState: PropTypes.oneOfType([PropTypes.object]), }; renderNotification() { @@ -18,12 +17,12 @@ export default class CommentScreen extends BaseComponent { if (!locationState || !locationState.redirectFrom) return null; + window.history.replaceState({}, document.title); + return ( -
    +
    You have been redirected from - - {locationState.redirectFrom} - + {locationState.redirectFrom}
    ); } @@ -41,18 +40,6 @@ export default class CommentScreen extends BaseComponent { actions={actions} ajaxCounter={data.get('ajaxCounter')} /> -
    ); diff --git a/client/app/bundles/comments/components/CommentScreen/CommentScreen.scss b/client/app/bundles/comments/components/CommentScreen/CommentScreen.scss deleted file mode 100644 index 1dc5a1eb..00000000 --- a/client/app/bundles/comments/components/CommentScreen/CommentScreen.scss +++ /dev/null @@ -1,17 +0,0 @@ -.notification { - padding: 1em 1.5em; -} - -.logo { - background: url('./images/railsonmaui.png') no-repeat left bottom; - display: inline-block; - height: 40px; - margin-right: 10px; - width: 146px; -} - -.twitterImage { - background: url('./images/twitter_64.png') no-repeat; - height: 64px; - width: 64px; -} diff --git a/client/app/bundles/comments/components/Footer/Footer.jsx b/client/app/bundles/comments/components/Footer/Footer.jsx new file mode 100644 index 00000000..2cb65281 --- /dev/null +++ b/client/app/bundles/comments/components/Footer/Footer.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import BaseComponent from 'libs/components/BaseComponent'; + +export default class Footer extends BaseComponent { + render() { + return ( + + ); + } +} diff --git a/client/app/bundles/comments/components/NavigationBar/CommentsCount.jsx b/client/app/bundles/comments/components/NavigationBar/CommentsCount.jsx index 115231f5..8609e99b 100644 --- a/client/app/bundles/comments/components/NavigationBar/CommentsCount.jsx +++ b/client/app/bundles/comments/components/NavigationBar/CommentsCount.jsx @@ -1,14 +1,19 @@ -import React, { PropTypes } from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; -const href = 'https://github.com/shakacode/react_on_rails/blob/master/README.md#multiple-react-' + +const href = + 'https://github.com/shakacode/react_on_rails/blob/master/README.md#multiple-react-' + 'components-on-a-page-with-one-store'; -const CommentsCount = (props) => ( -
  • - - Comments: {props.commentsCount} - -
  • -); +function CommentsCount(props) { + const { commentsCount } = props; + return ( +
  • + + Comments: {commentsCount} + +
  • + ); +} CommentsCount.propTypes = { commentsCount: PropTypes.number.isRequired, diff --git a/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx b/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx index 827aa4cb..65dc3b40 100644 --- a/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx +++ b/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx @@ -3,62 +3,123 @@ import classNames from 'classnames'; import _ from 'lodash'; -import React, { PropTypes } from 'react'; +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; -import CommentsCount from './CommentsCount'; +import CommentsCount from './CommentsCount.jsx'; import * as paths from '../../constants/paths'; -const NavigationBar = (props) => { +function NavigationBar(props) { const { commentsCount, pathname } = props; - /* eslint-disable new-cap */ + const [isOpen, setIsOpen] = useState(false); + + const menuWrapperClasses = 'flex flex-col lg:flex-row flex-wrap lg:items-center lg:visible'; + return ( -