Skip to content

Commit

Permalink
More docs and a few cosmetic code changes
Browse files Browse the repository at this point in the history
  • Loading branch information
maxott committed Sep 23, 2019
1 parent 59302d3 commit d01421f
Show file tree
Hide file tree
Showing 12 changed files with 528 additions and 109 deletions.
85 changes: 60 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Motivation

Most of the web frontends we are usually building are for a rather small user base to better use or maintain rather complex backends. Many of those systems start out small but over time expand in various directions by different teams using different technologies. Most likely a common scenario for many business support services.

We use micro services and similar technologies to avoid any unnecessary dependencies in the backend, but our users, understandably want a unified UX in the frontend.
We use micro services and similar technologies to avoid any unnecessary dependencies in the backend, but our users, understandably want a unified UX on the frontend.

This project is an attempt to achieve that while supporting the independent development of the various parts and components surfacing specific backend capabilites. In other words, we want to minimize the amount of code changes when adding new functionality while still supporting an integrated UX experience.

Expand All @@ -18,44 +18,69 @@ Let me explain that with a trivial example. Let us assume we just delivered an i
Approach
---

We have found the React/Redux approach to be extremely useful in managing dependencies between UX components and cleanly separating state synchronisation between front- and backend. In addition, a purely functional approach to component design not only leads to much cleaner code, but also simplifies testing considerably.
We have found the React/Redux approach to be extremely useful in managing dependencies between UX components and cleanly separating state synchronisation between front- and backend. In addition, a purely functional approach to component design not only leads to much cleaner code, but also simplifies testing considerably. We can visualize this as:

With the positive lessons learned from defining a web page as pure functions over a single state structure, we wanted to see if we can push this further and essentially select those functions through another function over a _UX_ state object.
![Standard Redux](doc/standard-redux.png)

![Basic Idea](doc/high-level.png)

In order to do that, we need to define a _construction_ model for a web page. In _Pihanga_, like in other frameworks, a page is composed of hierarchically nested **cards**. Or, in other words, a _tree of cards_ with the root of the tree being the entire page frame. Each card can contain normal web components as well as other cards. However, we restrict a card to define an embedded card only by a tree-globally unique name. In addition, each card is stateless and it's final presentation and behaviour defined by a set of externally provided properties (which may include the name to be assigned to an embedded card).
With the positive lessons learned from defining a web page as pure functions over a single state structure, we wanted to see if we can push this further and essentially derive the above function _ui()_ itself as the result of another function over the _UX_ state object.

The _Pihanga_ state structure, different to the _Redux state tree_, is a map between the _name_ of a card and it's associated property list. However, the values in that property list can be queries (currently functions) over the entire _Pihanga_ state (all other cards) and the _Redux_ state.
In order to do that, we need to define a _construction_ model for a web page. In _Pihanga_, like in other frameworks, a page is composed of hierarchically nested **cards**. Or, in other words, a _tree of cards_ with the root of the tree being the entire page frame. Each card can contain normal web components as well as other cards. Current practice will not only select the card
to be embedded in another card, but also declare all it's properties. For instance:

Let's demonstrate that on a simple app consisting of a frame-filling `page` card which will show one of two listing cards depending on the `route.routePath` property in the Redux state.
```
export const FooCard = (props) ==> {
...
return {
<>
...
<BooCard p1={...} p2={...} ... />
...
</>
}
}
```

And that's where _Pihanga_ departs from current practice. Instead of a declaring the exact card to embed, we employ a _late binding_ approach, where the embedded card is only identified by a locally unique identifier. The same code
segment as above in _Pihanga_ looks like:

```
export const FooCard = (props) ==> {
...
return {
<>
...
<Card cardName="Boo" />
...
</>
}
}
```
where `Card` is essentially a placeholder for a _Pihanaga component_ defined separately.

The _Pihanga_ state structure, different to the _Redux state tree_, is a map between the `cardName` of a card and it's associated property list. In addition, the values in that property list can be queries (currently functions) over the entire _Pihanga_ state (all other cards) as well as the _Redux_ state.

Let's demonstrate that on a simple app consisting of a frame-filling `page` card which will show one of two listing cards depending on the `showList` property in the Redux state which is expected to either contain `cars` or `trucks`.

The _Pihanga_ state structure is defined as follows:

```javascript
import flow from 'lodash.flow';
import { pQuery } from '@pihanga/core'card.service';
export default {
page: {
cardType: 'AppPage',
title: 'Transportation Service',
contentCard: flow(
pQuery(null, 'path', (s) => s.route.routePath),
(a) => a.length == 1 ? a[0].cardName : 'carListing'
),
subTitle: (_, ref) => ref('.', 'contentCard', 'title'),
contentCard: (state) => state.showList,
//...
},
carListing: {
cardType: 'Table',
cars: {
cardType: 'PiTable',
title: 'Cars',
path: '/cars',
//...
},
truckListing: {
cardType: 'Table',
trucks: {
cardType: 'PiTable',
title: 'Trucks',
path: '/trucks',
//...
},
}
Expand All @@ -68,25 +93,35 @@ import { Card } from '@pihanga/core';

export const AppPageCard = ({
title,
contentCard,
subTitle,
contentCard,
//...
}) => {
return (
<div>
...
<Card cardName={contentCard} />
<Card cardName={contentCard} />
...
</div>
);
};
```
The `contentCard` property in the `page` card definition shows the use of query to dynamically calculate the property value. `pQuery` returns an query over the _Pihanga_ state. `pQuery(null, 'path', (s) => s.route.routePath)` selects all card defintions (`null` wildcard) where the `path` property (if defined) is equal to the current _Redux_ value `route.routePath`. For the current setup, the query will return zero or one records which is reduced in the second function to `flow` either the name of the single matching card or a default card.
In this simple example the mapping between `<Card cardName={contentCard} />` and the actual card embedded is determined by the
value bound to the `contentCard` property of of the _Pihanga state_ of the card (`page`) embedding it. That value can either be a
constant (`title: 'Cars'`), a reference to the property value of another card (`... => ref('.', 'contentCard', 'title')`), or a function on the current _Redux_ state (`... => state.showList`).

Simply changing the value of `showList` to a different value, will not only change what card is being embedded inside `AppPageCard`, but also the `subTitle` that card is displaying. Beside the dynamic nature of this approach, we can now also
develop 'template' cards which can dynamically be adapted to various different uses __without__ having to change their internals.

## Further Reading

The [examples](./examples) directory contains various examples on how _Pihanga_ can be used to quickly develop useful and scalable web applications. See the _README_ files in the various sub directories for more information.

The

# Developer Notes
## Developer Notes

## Pulishing new release
### Pulishing new release

lerna version prerelease
lerna publish from-git
Binary file added doc/standard-redux.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 examples/micro-service/doc/wireframes.bmpr
Binary file not shown.
167 changes: 86 additions & 81 deletions examples/real-time-charts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,102 +37,107 @@ declares all the used cards, their properties and how it fits together.

But before we have a look at that, we need to understand the workflow implemented by this app.

# FIX ME
## Single Page App

As mentioned in the overview the app takes a user through three different step, `passport`, `spinner`, and `answer`. The currently active step is stored in the `step` property of the _Redux_ state (see [app.initial-state.js](src/app.initial-state.js)).
We build a very simple app with titled page containing a single grid layout card for the content, which in turn holds two cards dispaying a line chart, one for CPU and one for memory use.

Now let us get back to [app.pihanga.js](src/app.pihanga.js) and look at the entry point `page`.
![Wireframe](doc/wireframe.png)

page: {
cardType: 'PiSimplePage',
contentCard: s => s.step,
...
},

`PiSimplePage` is a card provided by the standard __Pihanga__ library and provides ust minimal scaffolding for displaying a single child card identified through `contentCard`. In our case we
simply use the workflow `step` from the _Redux_ state.
const page = {
page: {
cardType: 'PiPageR1',
contentCard: 'graphs',
title: 'Realtime Charts',
footer: {copyright: 'The Pihanga Team'}
},

The initial workflow state is `passport` and indeed we find a `passport` entry in [app.pihanga.js](src/app.pihanga.js):
graphs: {
cardType: 'PiGrid',
spacing: 3,
content: ['cpuGraph', 'memoryGraph', ],
},

passport: {
cardType: 'PiTitledPage',
contentCard: 'form',
...
},

form: {
cardType: 'PiForm',
title: 'Ask the Ring',
submitLabel: 'Send Query',
fields: [
cpuGraph: {
cardType: 'WrappedCard',
title: 'CPU User',
...
],
...
},
},

![Passport Page](doc/passport.png)

The `passport` page consists of a titled page and an embedded form (`form`). Pressing the 'submit' button will trigger the default action:

{
type: "PI_FORM:FORM_SUBMIT"
id: "form"
passport: "44444"
question: "0"
ring: "0"
memoryGraph: {
cardType: 'WrappedCard',
title: 'Memory',
...
}
};

The first two card definitions are rather straight forward, while the
card type `WrappedCard` is a locally defined _Meta Card_. A _Meta Card_ does not represents any actual cards, but will instead return a named list of new cards which are dynamically inserted into the card declaraton list. Please note, that any of the returned cards can be a
_Meta Card_ leading to a recursive expansion.

Now lets have a look at the definition of `WrappedCard` which we can
find at the beginning of [app.pihanga.js](src/app.pihanga.js):

```
export function init(register) {
...
register.metaCard('WrappedCard', (name, defs) => {
const {
cardType, title,
yLabel, metricsType, maxY = 100,
...inner
} = defs;
const innerName = `${name}-inner`;
const h = {};
h[name] = {
cardType: 'MuiCard',
title,
contentCard: innerName,
}

which is 'reduced' in [workflow.js](src/workflow.js):

register.reducer(actions('PiForm').FORM_SUBMIT, (state, action) => {
dispatchFromReducer(() => {
getPassportCount(action.passport);
});
const s = update(state, ['step'], 'spinner');
return update(s, ['question'], action.question);
});

The reducer first initiates an API call `getPassportCount`, and then changes both the `step`, as well
as the `answer` property of the _Redux_ state. Changing the `step` property to `spinner` will, according
to the `page.contentCard` declaration in [app.pihanga.js](src/app.pihanga.js), now display the `spinner` card
which is defined as:

spinner: {
cardType: 'Spinner',
...
h[innerName] = {
cardType: 'ReLineChart',
data: s => s.metrics[metricsType],
...
}
return h;
});
}
```

## Realtime Updates

[backend.js](src/backend.js):
```
const METRICS_URL = '/metrics?after=:after';
const UPDATE_INTERVAL_MS = 2000;
export function init(register) {
registerPeriodicGET({
name: 'getMetrics',
url: METRICS_URL,
intervalMS: UPDATE_INTERVAL_MS,
start: '@@INIT',
init: (state) => {
const m = {metrics: {...}};
return update(state, [], m);
},
'Spinner' is an application specific card and defined in the [spinner](src/spinner) directory.

![Spinner Page](doc/spinner.png)

The above referenced `getPassportCount` function will dispatch a `UPDATE_PASSPORT` event on successful
completion of the API request, which in turn is reduced in [workflow.js](src/workflow.js):

register.reducer(ACTION_TYPES.UPDATE_PASSPORT, (state, action) => {
const s = update(state, ['step'], 'answer');
return update(s, ['answer'], action.reply);
});

The first update is setting the step to `answer` which in turn will display the `answer` card defined in
[app.pihanga.js](src/app.pihanga.js) as follows:

answer: {
cardType: 'Answer',
answer: s => s.answer,
question: s => s.question,
request: (state) => {
const lastTS = state...
return {after: lastTS};
},
reply: onMetricsUpdate,
});
}
As with the `Spinner` card type, `Answer` is also an app specific card and defined in the [answer](src/answer) directory. It's properties `answer` and `question` are bound to the equally named properties in _Redux_.
function onMetricsUpdate(state, reply) {
...
return update(state, ['metrics'], metrics);
}
```

![Answer Page](doc/answer.png)

Finally, the action `NEW_REQUEST` associated with the `NEW REQUEST` button on the answer page is reduced in
[workflow.js](src/workflow.js) to return to the `passport` page:

register.reducer(ANSWER_TYPES.NEW_REQUEST, (state) => {
return update(state, ['step'], 'passport');
});



Expand Down
Binary file added examples/real-time-charts/doc/wireframe.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 added examples/real-time-charts/doc/wireframes.bmpr
Binary file not shown.
Loading

0 comments on commit d01421f

Please sign in to comment.