Skip to content

Commit

Permalink
Updated JS App to use WebPack to bundle JSApp into a single file.
Browse files Browse the repository at this point in the history
Added image assets.
Added Actions.
Added BEFFE recorders.
Added tooling for generating API Definitions.
  • Loading branch information
jezzsantos committed Oct 6, 2024
1 parent 04c57e5 commit 3da8dd6
Show file tree
Hide file tree
Showing 50 changed files with 38,519 additions and 5 deletions.
19 changes: 19 additions & 0 deletions README_DERIVATIVE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ You will need the following development tools to build, run, and test this proje
* Note: if using Visual Studio, the built-in Roslyn analyzers will not work (due to .netstandard2.0 [restrictions between Visual Studio and Roslyn](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview))

* Install the .NET8.0 SDK (specifically version 8.0.6). Available for [Windows Download](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/sdk-8.0.302-windows-x64-installer)
* Install NodeJs (18.20.4 LTS or later), available for [Download](https://nodejs.org/en/download/)

> We have ensured that you won't need any other infrastructure running on your local machine (i.e., a Microsoft SQLServer database) unless you want to run infrastructure-specific integration tests.
Expand Down Expand Up @@ -130,6 +131,24 @@ In the `Infrastructure.Shared.IntegrationTests` project, create a new file calle

> DO NOT add this file to source control!
## Build the Website

* `cd src\WebsiteHost\ClientApp`
* `npm install`
* `npm run build`

> Note: As a result of this build step you should see new bundle file (e.g. `0123456789abcdef.bundle.js`) appear in the
`wwwroot` folder. This file should never be added to source control.

### Environment Variables

You need to create your own version of the `.env` file on your computer (not source controlled).

1. Copy the `src/WebsiteHost/ClientApp/.env.example` to `src/WebsiteHost/ClientApp/.env`.

> DO NOT add this file `.env` to source control! This files exists locally for security purposes, and in order to have
> the right environment variables in place when running and testing the JS App.
# Build & Deploy

When pushed, all branches will be built and tested with GitHub actions
Expand Down
72 changes: 72 additions & 0 deletions docs/decisions/0170-javascript-actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# JavaScript Actions

* status: accepted
* date: 2024-10-05
* deciders: jezzsantos

# Context and Problem Statement

In the design and implementation of FrontEnd JavaScript clients, developers often spend a lot of time writing boilerplate code to perform the same tasks to improve the usability of the UI.

In many cases, many of the standard considerations of improved usability that should be common across all UIs are forgotten by the developer as they focus only on getting the UI and controls they care about - just working. Once released, and in the wild, the end user often experiences poor usability for very basic needs. This damages the perceived quality of the product. Here are some very common examples:

Example 1: Offline status

The device that the browser is running on (i.e., a desktop, a mobile phone, etc.) loses internet connectivity, and the UI does not prevent the user from submitting data/input they are working with (sometimes that can be a significant amount of data the user had to provide). The UI will fail in some way when a command that the end user makes requires an API call across the internet. The API call will eventually timeout (offline), and then it's the question of how the developer designed the code to handle this error, if at all. Here are some possibilities:

* The developer didn't bother to design for this scenario at all. The API call is issued when the browser is 'offline', and returns to the JS with an 'offline' error, but the error was not handled, the UI likely did nothing, and the user is unaware that there was an error at all.
* The end user waits, and waits, confused that the app is unresponsive to them, and they likely retry the command (click the "submit" button) over and over again as if to "wake up" the unresponsive app to respond to them immediately.
* Obviously, this may work in some cases for the end user, but it is also likely that the command they issued will be tried and retried over and over again, and even if the browser re-establishes connection during this process, or not this may also cause subsequent errors, but they remain unaware of those also.
* If the connection is still offline, the user wont even be aware of that, unless they check their device.

A better way to handle this scenario is to let the end-user know they have lost connection as soon as it occurs or wait until any API call indicates 'offline' status and let the user know about it until the connection is restored again.

Then, to take it a step further, prevent the user from making the API call in the first place until the connection is restored. In other words, disable the controls that make API calls, until the connection is restored.

At the very least, handle the error properly, informing the user that they are offline and asking them to retry when 'online' status has been established.

Furthermore, when API calls are being made, and we have 'online' status, we also would want to do this:

1. Indicate the API is being made, by indicating busy status. For example, a spinner on the 'Submit" button
2. Disable the "Submit" button until the API call returns (except for offline indicators)

In all web forms, we would expect all the conditions to be handled for the developer automatically, and ideally they never have to think about it. It would be built in for them.

Example 2: Form Validation

When an end user is working with a form, and entering user input, we would expect that invalid input would be detected as soon as it is entered. We would expect the form to indicate that and we would expect the form to prevent submission until all data is present and valid. Many developers forget to wire all that up correctly, and some allow forms to be submitted by calling API's that return
`HTTP 400 - BadRequest` error, and thus they allow the user fail the command.

The basic design principle should be that the client application should NEVER allow a `HTTP 400 - BadRequest` error to be returned from an API call (except in uncontrollable edge cases), because the client should work be designed very meticulously to guard against that happening to the end user.

A better experience for the end user is to disable the "submit" button that gathers the user's input and issues the API call until all the data (and context) is valid.

Example 3: Unexpected/Expected Errors

In general, developers, under pressure, usually only design for the happy path and often forget to design for exceptional cases. This is very common for developers who don't design their software with automated tests; thus they are not reminded to consider what could go wrong. Further, there is great effort when only manually testing UIs even for the happy paths, let alone all the error cases.

What is needed is to ensure that no matter what happens, when an API call fails to execute, perhaps it is a timeout (due to network connectivity), perhaps it is an unexpected defect in the API (i.e., a `HTTP 500 - InternalServerError`) or perhaps it is a legitimate
`HTTP 405 - MethodNotAllowed` error from the API saying that something isn't in a state to be changed. No matter what, the UI is responsive to the error, and the end user is notified that there was an error and, ideally, what to do about it.

Last point to make about this. It is a well-known design principle in clients that errors that are returned from API calls and servers are not designed to be consumed by end users. They are designed to be consumed by the developers building the clients. Therefor, developers, should never just pass on the error to the end-user that they get from the backends. Many, many developers forget this and do what's expedient, and the end user has a terrible time trying to figure out what they can do with these esoteric developer-oriented errors.

## Considered Options

The options are:

1. Provide a standardized, easy-to-apply, and consistent mechanism to handle all these common cases.
2. Leave it up to each engineer to know about and handle all these cases individually and in bespoke ways.

## Decision Outcome

`Standardized`

- We can enforce this as the basic building block of UI components, supported by a set of common components, and we can define common patterns of implementation to follow, and easy to extend.
- Handling offline capability is a general concern and can be made into a general mechanism across the codebase.
- Handling validation is very common, too, and can be baked into all interactive forms
- Handling XHR API calls can be easily standardized and made extensible for all use cases.
- The end users get a consistent and responsive set of experiences, particularly in exceptional cases.

## (Optional) More Information

We have decided to define the "[JavaScript Action](../design-principles/0200-javascript-actions.md)" to provide the standardized experience we are after.
95 changes: 95 additions & 0 deletions docs/design-principles/0200-javascript-actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# JavaScript Actions

## Overview

JavaScript "Actions" are a strategy (in web-based clients) that provides consistency and durability in user interfaces, which results in better usability of a product in a way that helps engineers create new user interfaces quickly and reliably.

Building a user interface is a complex activity, usually focused on getting something working per design. In this pursuit, it is very easy for an engineer to handle very basic usability issues like handling errors, dealing with network connectivity, and helping the user "fall into the pit of success," as opposed to allowing them to easily fail to do whatever it is they want to achieve by using the product.

End users really don't want to be using systems that fail often, and in the internet-dominated world of unreliable connectivity, this is quite tricky to achieve well without armies of engineers and testers in hand.

What is needed is a general-purpose mechanism that helps engineers focus on solving the happy path, which is backed up by built-in mechanisms that handle the unhappy paths.

That is exactly why we have designed "JavaScript Actions".

(Read: this [Design Decision](../decisions/0170-javascript-actions.md) for more examples on why this mechanism is required)

## Implementation

Here is a JavaScript Action.

![JavaScript Action](../images/FrontEnd-Action.png)

The action is used in all interactive UI pages/components that make calls to backend APIs.

The action is a simple 'class' derived from a base implementation, either wired into a JavaScript framework or into custom JavaScript components.

The action is always bound to:

1. An `IOfflineService`, that monitors the 'online' status of the browser/device
2. An XHR API call with a defined set of expected errors with end-user specific messages, an optional set of unexpected errors with end-user specific messages, and an option set of success codes (per every API call).

The action can optionally be bound to:

3. A UI form, that uses action-enabled for controls, and components

### How It Works

Regardless of the JavaScript architecture or JavaScript framework itself (e.g., VueJs, ReactJs, JQuery, etc.) or hand rolled, there are some default behaviors that Actions must perform.

> Implementation details vary, but all these behaviors can be implemented with any of these frameworks or approaches.
#### Offline Handling

The `IOfflineService` actively monitors the browser's network connectivity.

* This service actively/passively detects when connectivity is lost using various strategies (i.e., polling),
* It maintains a state of 'online' or 'offline'.
* This service must also updated whenever any XHR call fails due to loss of connectivity, and when it is, it must then actively detect when connectivity is restored.

The action is always wired into this `IOfflineService`:

If 'offline':

* The UI will indicate that the user experience is offline (e.g., a banner on top of the page).
* No XHR API call can be made. See "XHR Handling" below for more details.
* If the action is bound to a `<Form/>`, then see "Form Handling" below

If 'online':

* The UI will indicate that the user experience is online (e.g., hide the offline banner on top of the page).
* The bound XHR call will be allowed to be made. See "XHR Handling" below for more details.
* If the action is bound to a `<Form/>`, then see "Form Handling" below

#### XHR Handling

All actions are bound to a mandatory XHR definition, which will define an HTTP request, a URL, and optionally collections of 'expected' and 'unexpected' errors, with messages to be shared with end users, should they arise.

* The XHR call can be made programmatically by the action.
* The bound XHR call will be allowed to be made only when the `IOfflineService.status` is 'online'
* When the call is being made, the action will block the XHR call from being made again until the existing call is returned.
* If the action is bound to a `<Form/>,` then:
* The `<Submit/>` control will be wired up to invoke the XHR call.
* When the XHR returns an 'offline' error, the `IOfflineService` status is updated.
* See "Form Handling" below for more details

#### Form Handling

The action can be optionally bound to a `<Form/>`; when it is, there are a number of form-based controls that can interact with the Action, to handle "offline" status, XHR status, and any error responses, and handle form validation.

These three integrated mechanisms do much of the heavy lifting for engineers. All they have to do is wire them up correctly.

When the `IOfflineService.status` is "offline":

* The `<Submit/>` control will be disabled.
* The `<Form/>` will indicate that the user experience is offline (e.g., display a footer text under the `<Submit/>` button)

When the `IOfflineService.status` is "online":

* The `<Submit/>` control will be enabled.
* The `<Submit/>` button can invoke the XHR call, and when it does:
* The `<Submit/>` button will be disabled
* The `<Submit/>` button will display a "busy" cue (e.g., a spinner on the button or a spinner on the page)
* When the XHR is returned (success or error), the `<Submit/>` button is enabled again, and the "busy" cue is hidden.
* When the XHR returns an 'unexpected' error, the `<UnexpectedError/>` control is updated with a general error (e.g., one that asks the user to opt into reporting the error) and other relevant context to encourage the user to submit a user-defined crash report for the support team. Also, the error must be logged to the `IRecorderService.crash()`.
* When the XHR returns an 'expected' error, the built-in `<Error/>` control (on the `<Form/>`) is updated with the error message. Or an appropriate page is navigated to. Or another action is used specific to that error.
Binary file added docs/images/FrontEnd-Action.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/Sources.pptx
Binary file not shown.
Binary file added docs/images/branding/SaaStack.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6,508 changes: 6,508 additions & 0 deletions docs/images/branding/SaaStack.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/SaaStack.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -1759,6 +1759,7 @@ public void When$condition$_Then$outcome$()
<s:Boolean x:Key="/Default/UserDictionary/Words/=awrongtoken/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=azip/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=BEFFE/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=browserconfig/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Camelcased/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=charindex/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=creds/@EntryIndexedValue">True</s:Boolean>
Expand All @@ -1781,7 +1782,9 @@ public void When$condition$_Then$outcome$()
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mediat/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mixedcase/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mqif/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=msapplication/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mship/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mstile/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=NHMAC/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=notaboolvalue/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=notacountrycode/@EntryIndexedValue">True</s:Boolean>
Expand Down
5 changes: 5 additions & 0 deletions src/WebsiteHost/ClientApp/.env.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# These placeholder values will be replaced by your CD platform for deploying to production
APPLICATION_INSIGHTS_INSTRUMENTATION_KEY="#{ApplicationInsights:InstrumentationKey}"
CURRENT_ENVIRONMENT="#{WebApp:CurrentEnvironment}"
BASEURL="#{WebApp:BaseUrl}"

3 changes: 3 additions & 0 deletions src/WebsiteHost/ClientApp/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
APPLICATION_INSIGHTS_INSTRUMENTATION_KEY=
CURRENT_ENVIRONMENT=development
BASEURL=http://localhost:5101
22 changes: 22 additions & 0 deletions src/WebsiteHost/ClientApp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# dependencies
npm-debug.log*
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# environment variables
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.env

# tests
junit.xml
30 changes: 30 additions & 0 deletions src/WebsiteHost/ClientApp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# WebsiteHost JS App

## Packaging & Bundling

We use [WebPack](https://webpack.js.org/) by default to provide bundled assets in a single JS file, which is output to
the `wwwroot` folder, where some other static assets live (e.g. SEO images, and SEO configuration).

### Browser Caching

To ensure that we are updating our customers' browser each and every time the JS code changes, we must bust the browser
cache for the generated `bundle.js` file.

This is done by pre-pending a digest to the generated `???.bundle.js` file, and updating that in the server-side
`Index.html` file.

To make that possible, we use a webpack plugin called `assets-webpack-plugin` which outputs the webpack generated asset
metadata into a `webpack.build.json` file, which lives in the code.
Then, at runtime, we load this JSON file dynamically, read out the digest, and update the `Index.html` file with the new
digest (in `Index.cshtml`).

## API Definitions

We use AXIOS to call the APIs in the BEFFE.

We generate these services automatically for you, by examining the BEFFE API and the BACKEND APIs, and then generate the
services for you.
You can update those at any time by running `npm run update:apis`

For this to work properly you must run both the BEFFE and the BACKEND APIs locally, so that the OpenAPI swagger endpoint
is reachable.
Loading

0 comments on commit 3da8dd6

Please sign in to comment.