Manage and display interesting information about engagements.
Users can sign up and then log in, after which they can create projects. Projects can be shared with other users via their email addresses, with a simple role-based permission system.
Projects are containers for updates of various types. Updates can be added via the web UI or through an integration with a third party system. Update types include:
- Information about a release.
- New insights.
- Goals for teams.
- Important RAID items: risks, issues, dependencies, decisions, etc.
- Flow data that can be used to render pretty graphs.
Updates contain some common data - a title, a description, a date - and some information specific to the update type.
Updates can be added via a simple RESTful API, secured with unique tokens. A
project owner can generate one or more tokens, which should be kept as secret
as possible. A POST
request can then be sent to the endpoint URL, which will
(depending on the hosting) be:
https://{domain}/api/post-update
The request must have a Content-Type
of application/json
and contain a JSON
object in its payload. This object should have the following keys:
token
– The unique project token. This serves both to authenticate the request, and to identify the project to update.updateData
– An object containing the data needed to create or modify an update – more on this below.alwaysCreate
(optional) – Always create a new update, rather than modifying an existing one, even if the data inupdateData
would normally imply updating one.
The update data is specific to the update type, but all include certain fields:
"token": <token string, required>,
"updateData": {
"type": <string, required>,
"title": <string, required>,
"summary": <string, optional>,
"date": <date, required>,
"team": <string, optional>
...
}
(Note: To encode a string, use a JavaScript-style ISO date string, e.g.
"date": "2020-01-02T00:00:00.000Z"
).
Depending on the type
, other fields are required:
Insights:
"updateData": {
"type": "insights",
"title": <string, required>,
"summary": <string, optional>,
"date": <date, required>,
"team": <string, optional>,
"authorId": <string, required>,
"authorName": <string, required>,
"text": <string of markdown-formatted text, required>
}
Goals:
"updateData": {
"type": "goals",
"title": <string, required>,
"summary": <string, optional>,
"date": <date, required>,
"team": <string, optional>,
"authorId": <string, required>,
"authorName": <string, required>,
"text": <string of markdown-formatted text, required>
}
Release:
"updateData": {
"type": "release",
"title": <string, required>,
"summary": <string, optional>,
"date": <date, required>,
"team": <string, optional>,
"releaseDate": <date, optional>,
"status": <string, one of: 'in-progress', 'complete', 'overdue'>
"text": <string of markdown-formatted text, required>
}
RAID update:
"updateData": {
"type": "raid",
"title": <string, required>,
"summary": <string, optional>,
"date": <date, required>,
"team": <string, optional>,
"raidItems": [{
"type": <string, one of: 'risk', 'issue', 'assumption', 'dependency', 'decision'>,
"summary": <string, required>,
"url": <string, optional, must be a valid URL>,
"priority": <string, one of: 'low', 'medium', 'high'>,
"date": <date, optional>
}]
}
Flow:
"updateData": {
"type": "flow",
"title": <string, required>,
"summary": <string, optional>,
"date": <date, required>,
"team": <string, optional>,
"cycleTimeData": [{
"item": <string, required>,
"commitmentDate": <date, required>,
"completionDate": <date, optional>,
"itemType": <string, optional>,
"url": <string, optional, must be a valid URL>
}]
}
In all cases, you can also add an "id"
parameter to refer to a specific
update (the ID is visible in the URL in the web app), in which case this will
be modified.
Specifically for the flow update type, the default behaviour is to modify
another flow update for the same team, i.e. matching on the "team"
field.
To avoid this behaviour, send "alwaysCreate": true
in the API payload.
You can also use the API via a simple command-line tool. This also serves as
a simple example of how to call the API. It can be found in the cli/
folder,
which is its own NPM package. To install the tool:
$ cd cli
$ npm install -g
This should install a utility called engagement-dashbaord
:
$ engagement-dashboard --help
The basic usage pattern is:
$ engagement-pattern --url <API endpoint URL> --token-file token.txt --data-file data.json
The --token-file
parameter specifies a text file that contains the token (we
do it this way because it is easier to secure a file than a command line
parameter, which might be replicated in shell history etc.).
The --data-file
parameter specifies a JSON-formatted text file that contains
the update payload (i.e. the contents of the "updateData"
parameter).
The client side uses ReactJS and is managed with create-react-app.
The server side uses Google Firebase (Firstore, Cloud Functions, Authentication).
- NodeJS (v13.12 tested)
- Firebase CLI (
npm install -g firebase-tools
) - Install all dependencies with
npm install
in the root directory - A Java JDK for running the firebase emulators locally
The remote Firebase project and associates access keys are deliberately not checked into source control.
A new environment can be created in the Firebase web console, and should have the following enabled:
- Cloud Firestore
- Email-based authentication
- Hosting
- A web app
You can also use the firebase CLI to create these, but note that the generated
configuration in firebase.json
is under source control. The .firebaserc
file that references the remote project is not, however. It should be in the
root of the repository and contain something like:
{
"projects": {
"default": "<project-id>"
}
}
Additionally, you need to configure a number of environment variables for the
React compiler to be able to interpolate variables that will allow the Firebase
JavaScript client to connect to the server. To set these up, create a file
called .env.local
in the root of the repository with:
REACT_APP_API_KEY=<api-key>
REACT_APP_AUTH_DOMAIN=<domain>.firebaseapp.com
REACT_APP_DATABASE_URL=https://<app>.firebaseio.com
REACT_APP_PROJECT_ID=<app>
REACT_APP_STORAGE_BUCKET=<app>.appspot.com
REACT_APP_MESSAGING_SENDER_ID=<id>
REACT_APP_APP_ID=<id>
The relevant values can all be found in the Firebase web console.
You can also point the local app at the Firebase emulators if you run them. This
requires two additional environment variables (in the .env.local
file or in
the terminal environment used to launch the webapp with npm start
):
REACT_APP_EMULATE_FIRESTORE=localhost:8080
REACT_APP_EMULATE_FUNCTIONS=http://localhost:5001
The emulators must be started before the webapp with:
$ firebase emulators:start
(or if you prefer: npm run emulators
)
Various npm
scripts can be used to start the app for local development and
testing, as well as to deploy it to Firebase.
To run the app locally (connecting to a remote Firebase project as per the configured environment):
npm start
will run the webapp locally athttp://localhost:3000
To run tests in interactive "watch" mode:
npm test
will run unit tests in watch mode, automatically re-running tests when files are changed.npm run test:models
will run unit test the shared models package in watch mode.npm run emulators
will start the Firebase emulators, after which you can run:npm run test:integration
to run integration tests in watch mode.
To run the tests once:
npm run test:ci:all
will run all of the below in sequence:npm run test:ci
for the front-end tests.npm run test:ci:models
for the models unit tests.npm run test:ci:integration
for the integration tests including starting the emulators (this will fail if the emulators are already running!)
To build and deploy to production:
npm run build
will bundle the webapp intobuild
for deployment.npm run deploy
will build the app (callingnpm run build
) and then deploy it to Firebase hosting, alongside Cloud Functions (in thefunctions
directory) and Firestore rules. To deploy functions only, runnpm run deploy
from thefunctions
directory.
The code is organised as follows:
-
firebase.json
configures the Firebase command line client and signposts which parts of Firebase are in use. -
firestore.rules
andfirestore.indexes.json
configure the Firestore database. The rules use a DSL (see the Firebase documentation) to determine what is and isn't allowed by the client API. In part, this helps define the "shape" of the data that is allowed (although the data store is schema-less so this is imperfect) and in part it enforces all the security rules about who can read and write what data. -
public/
contains static HTML assets which are bundled into the client-side app build. Notably the mostly-emptyindex.html
page where the React component tree is mounted. -
src/
contains the client-side React webapp. When built, it can be served as static HTML. Connecetions to the backend are made using the Firebase Javascript client. The webapp consists of:api.js
, which contains a singleton class that performs all the Firebase backend operations. React components that need access to the backend calluseAPI()
to get a handle to this API and then call its various functions.App.jsx
, the root of the application, which initiates client-side page routing (usingreact-router-dom
).pages/
, which contains React components that represent pages in the application. Pages are mounted usingreact-router
fromApp.jsx
.layouts/
, wherein there are layout components for anonymous and authenticated users – common UI such as the toolbar that pages wrap themselves in.components/
, which contains smaller, reusable React components.components/updates/
, wherein live the various views (summary tile, full-page view, add/edit form) for each of the update types. These all export an object that is imported incomponents/updates/index.js
and used in a registry of update type views keyed by an update type string stored in the database (more on this below).utils/
contains shared utility code.
-
functions/
contains Firebase Cloud Functions. It is a self-contained NPM package and uses slightly different (more basic, less cutting-edge) transpilation and linting settings from the React app. This can be confusing, so try to keep code in Cloud Functions simple and avoid using experimental JavaScript features. -
functions/models
is a separate NPM package that contains some classes that define the data model for the application. It has minimal dependencies and deliberately does not use any Firebase-specific libraries. This allows it to be shared by the client and Cloud Functions. It also has its own test suite. (It lives underfunctions/
because of the way Firebase deployment works.) -
integration-tests/
contains a set of tests that depend in the Firebase emulator suite running. Since they site outside the webapp, these too are only allowed to use a more basic set of JavaScript features, so keep them simple.
As an example that threads through all of these moving parts, let's consider how we would change the fields of one of the update types in the data model, the database, the tests, and the UI:
- First, find the relevant update type in
functions/models/*.js
, e.g.releaseUpdate.js
. This defines a validation schema (using theYup
library), declares class that derives fromUpdate
(which in turn derives fromBase
) that provides various methods for constructing and serialising an instance of the model, and registers itself with the update registry. To add a new field, we need to modify theYup
schema. - This will likely break the tests in
models/releaseUpdate.test.js
, so these will need to be run (npm test
from within themodels
directory) and adjusted as necessary. - Next, the integration tests will likely (hopefully!) break, because the
default object shape no longer conforms to the shape expected in the
Firestore rules. These live in
firetore.rules
. It is possible to modify and test these using the Firebase web console, but the safest way is to run the integration tests (runnpm run test:run-emulators
once from the root of the repository, thennpm run test:integration
in a separate terminal). The Firestore emulator automatically watch for changes to thefirestore.rules
file, so changes can be made and tested in real time. The test for the Release update type are inintegration-tests/releaseUpdate.test.js
. Once you have modified the rules, they can be deployed to the backend by runningfirebase deploy --only firestore:rules
. - Next, you will likely want to modify the add/edit form for this update type,
and possibly the summary and full-page view as well. These all live in
src/components/update/release.jsx
. The form uses theformik
library (and theformik-material-ui
UI bindings) to handle validation and other details, and Material UI components to render the content.