Code Red is a simple project management app, using a graph-based approach to task and resource management. This app was built for the Build on Redis Hackathon 2021.
A demo server is publicly available at https://codered.pm/, the database resets to sample data every 15 minutes.
Code Red is a simple app that allows you to manage and visualize your heavily interdependent project using a directed graph.
A project consists of a single graph, containing many tasks. A task can be an idea, goal, epic, feature, simple task or bug. It has a status as well: to do, in progress, in review or done.
Tasks can be linked to each other using a generic link (related to), or a specific relationship (blocked by, child of).
The application is conceived as a single Ruby on Rails monolith. For persistence, the application stores its data both relationally and in a graph: the former using PostgreSQL, the latter in RedisGraph, a Redis module by RedisLabs. Administrative data (such as users and projects) is stored relationally, while storage of tasks and the relationships between them is delegated to the graph storage.
The graph storage layer features a small custom built DSL, that translates method calls in the style of ActiveRecord into RedisGraph queries.
The web app is plain HTML sprinkled with some JavaScript (Stimulus for interactivity and D3.js/cola.js for graph visualization). The HTML is rendered server-side before being sent to the client. This means that instead of using JSON to transfer data between the server and client, HTML is sent and only the part of the DOM that changes, is replaced. Please refer to Turbo and Stimulus documentation for more information.
Graph data is fetched from a JSON endpoint using D3's JSON plugin. In order to keep the application fast and snappy, Hotwire is used as a framework.
Finally, the UI is built using TailwindCSS, Heroicons, Collecticons and QuillJS for the rich text editor.
Project
Projects are stored relationally in PostgreSQL. A project has the following properties:
id
: User identifieruser
: Owner of the projectname
: Human readable name of the project
The project is linked to a graph (using id
as graph name)
A graph has many tasks.
Task
Tasks are stored as nodes in Redis Graph. A task is a graph node and has the following properties:
title
: Task titledescription
: Rich text, multiline description of the taskdeadline
: Datestatus
: One ofTodo
,In Progress
,Review
orDone
type
: One ofIdea
,Goal
,Epic
,Feature
,Task
orBug
-user
: Assignee
A task can be linked to many other tasks, by relationships.
Relationship
Relationships are stored as edges in Redis Graph. A relationship is a directed graph edge and has the following properties:
from
: Source nodetype
: One ofBlocked By
,Child Of
,Related To
to
: Destination node
Relationships are stored as directed edges, but in the interface both directions are rendered. For example, if Task A is blocked by Task B, Task B will be shown as "blocks Task A". It is also possible to add relationships in both directions.
Fetch tasks
A graph has many tasks, which are fetched using the following Redis Graph query:
"GRAPH.QUERY" "055616f0-a130-42b1-a3fd-81b7c8a3ef1b" "MATCH (n:Task) RETURN n" "--compact"
Create/update task
A task is created and updated with all its properties using the following query:
"GRAPH.QUERY" "055616f0-a130-42b1-a3fd-81b7c8a3ef1b" "MERGE (n:Task {id: 'f5ec1f25-0cee-49d0-9a85-1043f04ea845'}) SET n.created_at = '2021-05-15 10:20:28 UTC', n.updated_at = '2021-05-15 10:20:28 UTC', n.graph = '#<Graph name=055616f0-a130-42b1-a3fd-81b7c8a3ef1b>', n.id = 'f5ec1f25-0cee-49d0-9a85-1043f04ea845', n.title = 'Submit hackathon app', n.description = '<p>Description of my task</p>', n.deadline = '2021-05-15', n.status = 'todo', n.type = 'task', n.user_id = '25714246-be92-4d96-b1ce-cbb57aaf4747'" "--compact"
Delete task
A task is deleted using the following query:
"GRAPH.QUERY" "055616f0-a130-42b1-a3fd-81b7c8a3ef1b" "MATCH (n:Task {id: 'f5ec1f25-0cee-49d0-9a85-1043f04ea845'}) DELETE n" "--compact"
Fetch relationship
A task's related nodes are always queried based on relationship type. The related tasks are fetched using the following query:
"GRAPH.QUERY" "055616f0-a130-42b1-a3fd-81b7c8a3ef1b" "MATCH (n:Task {id: 'c9bc52a0-c436-499c-954c-da40e82f50b2'}) -[r:blocked_by]-> (m:Task) RETURN n, m, type(r) AS t" "--compact"
Add relationship
Two tasks are linked to each other using the following query:
"GRAPH.QUERY" "055616f0-a130-42b1-a3fd-81b7c8a3ef1b" "MATCH (n:Task {id: '1ad21814-69d7-47d0-a7bb-de678b86c653'}), (m:Task {id: '07427e6b-7bba-44e4-b967-8fb5ca098053'}) MERGE (n) -[r:blocked_by]-> (m)" "--compact"
Delete relationship
Two tasks are unlinked from each other using the following query:
"GRAPH.QUERY" "055616f0-a130-42b1-a3fd-81b7c8a3ef1b" "MATCH (n:Task {id: '1ad21814-69d7-47d0-a7bb-de678b86c653'}) -[r:related_to]-> (m:Task) DELETE r" "--compact"
A small Domain Specific Language was built to accommodate and simplify graph persistence. It aims at providing a small but robust interface that should feel familiar to developers used to ActiveRecord's API. The main class implementing this construction can be found at app/graph/dsl.rb.
Example of a query:
query = graph
.match(:n, from.class.name, id: from.id)
.to(:r, type)
.match(:m, to.class.name)
.delete(:r)
query.to_cypher
# => "MATCH (n:Task {id: 'c9bc52a0-c436-499c-954c-da40e82f50b2'}) -[r:blocked_by]-> (m:Task) DELETE r"
query.execute
# => []
First, ensure you have a working Docker environment.
Pull the images and start the containers:
docker-compose up -d
Compile the frontend code:
docker-compose run --rm app bin/webpack
Set up the PostgreSQL database:
docker-compose exec app rails db:setup
Load sample data into the PostgreSQL and Redis databases:
docker-compose exec app rails database:seed
The application should now be available at http://localhost:3000.
Use the bin/update
script to update your development environment dependencies.
If you want to enable faster compilation of assets, run Webpack dev server in the same container as the Rails server:
docker-compose exec app bin/webpack-bin-server
To debug the server component in your IDE, start the debug
instead of the app
container, and connect to localhost:1234
.
Run the test suite:
rspec
Github secrets for release:
DOCKER_TOKEN
(needed for Github Container Registry)
Github secrets for continuous deployment (process):
-
DOCKER_TOKEN
(needed for Github Container Registry) -
GANDIV5_API_KEY
(needed for Let's Encrypt integration) -
SECRET_KEY_BASE
-
SSH_HOST
(deployment host) -
SSH_USER
(deployment user) -
SSH_KEY
(private key) -
SSH_HOST_KEY
(host public key)
Update the changelog and bump the version in lib/code_red/version.rb
.
Create a tag for the version and push it to Github.
A Docker image will automatically be built and pushed to the registry.
nano lib/code_red/version.rb
git add lib/code_red/version.rb
git commit -m "Bump version to v1.0.0"
git tag v1.0.0
git push origin master
git push origin v1.0.0
See LICENSE.md.