
Energy Management Application
Energy Management intelligently optimizes the heat- and power-generation and allows customers to participate in various energy-markets.
Development
Prerequisites
IDE/VSCode setup
- Install the VS Code extension ZipFS - a zip file system (arcanis.vscode-zipfs)
- Should VScode report typescript-, prettier- or eslint-errors:
- Run
yarn pnpify --sdk
- Select typescript from the workspace
Code formatting
This project uses prettier to automatically format the source code.
- Install the VS Code extension Prettier - Code formatter (esbenp.prettier-vscode)
- Activate "Prettier: Require config" in VS Code settings to avoid formatting in other projects
- Activate "Editor: Format On Save" in VS Code settings to activate formatting on every save
Tailwind CSS IntelliSense and Twin
-
Install the VS Code extension Tailwind CSS IntelliSense
-
From your command palette select: "Preferences: Open Settings (JSON)" and add this to your settings:
"tailwindCSS.experimental.classRegex": [
"tw`([^`]*)", // tw`...`
"tw=\"([^\"]*)", // <div tw="..." />
"tw={\"([^\"}]*)", // <div tw={"..."} />
"tw\\.\\w+`([^`]*)", // tw.xxx`...`
"tw\\(.*?\\)`([^`]*)" // tw(Component)`...`
],
"tailwindCSS.includeLanguages": {
"typescript": "javascript",
"typescriptreact": "javascript"
}
Local Development
Note:
- The app requires a redis-server. The connection can be configured via config and defaults to
127.0.0.1:6379.
- In order to receive FCM messages during development the flag
--serviceWorker has to be passed to the watch command and the fcmConfig-config updated.
- The sync feature for the inbox relies on a redis pubsub mechanism. In order to make this work during development a tunnel to the pubsub instance is required and the inbox.pubsub-config has to be updated accordingly.
export YARN_NPM_AUTH_TOKEN=<<< your auth token >>>
yarn
yarn watch
Implementation details and constraints:
- The top-level lib-folder provides functionality for the server- and client-portion of the app.
It mustn't import anything from the client-folder. There are 2 exceptions to this: types and files within client/i18n/ may be imported.
- The app is built with typescript 4.x, reactjs 17.0 and primarily uses material-ui.
- While there is a css-loader in place, styling should primarily happen with emotion and/or makeStyles (provided by material-ui)
- Low-level styling can and should use tailwindcss via the twin.macro.
- Flexbox (whether it's used via tailwindcss or material-ui) should only be used where it makes sense, but not for grid-layouts. Grid layouts should only be built with CSS Grid Layouts. Tailwindcss simplifies that quite a bit: https://tailwindcss.com/docs/grid-template-columns/#app.
- Configurable dashboards should utilize the DashboardProvider/Dashboard components
- Drag-and-drop functionality should be built with react-dnd
- Forms should be built with Formik
- Data should primarily be handled via graphql. Use
yarn codegen to automatically create typescript definitions
Dashboards
Dashboards are the primary building blocks of the app. A Dashboard contains a set of widgets that are arranged in a grid-layout.
Dashboards are always responsive and have to specify a layout for 3 supported device sizes:
| xs | 0 | Mobile in portrait mode |
| md | 768 | iPad in portrait mode |
| xl | 1280 | Desktop |
While dashboards may be at any level of the navigation hierarchy, may look different and show different data, they provide a common behavior (drag/drop, resize, add/remove/change widgets, ...) to the user. Those behaviors and the layout-ing is implemented in client/components/core/dashboard.
It's important to understand that dashboards and widgets are meant to be configured by the user. Therefore the dashboard as well as the widget defines meta-data that governs if and how much a user can configure (e.g. min-width, max-width, ...). In addition a widget can store user-specific configurations of a widget within a given dashboard via it's config prop.
Widget Definition
As previously explained a dashboard consists of widgets. In order to define a widget the following steps have to be performed:
-
Scaffold structure:
Every widget is contained in a folder of the same name within client/components/widgets/. At a minimum it should contain:
-
Widget.tsx: The parent react component of the widget. It must be a functional component which receives props of the generic type RenderedWidget<T>. T extends WidgetConfig<Record<string, any>, any> and represents the configuration of the widget and defaults to an empty object {}.
import { RenderedWidget } from '../../core/dashboard';
export function Widget(props: RenderedWidget) {
return (
<div>Your content goes here</div>
)
}
The configuration-object has to follow these rules:
- a config object MAY be
undefined or MAY provide a configuration
- if it provides a configuration it MUST provide a
default key
- a config object MAY contain a key
state which is reserved for state which should be persisted
Note: RenderedWidget provides gridItemWidth and gridItemHeight which can be used by the component to do size calculations.
-
definition.ts: Must export a constant of type AvailableWidget<TConfig, TConfigState>. This contains all meta-data of the widget like componentName, column-count, preview images for the catalog, layouts for the various screen-sizes etc. It also specifies for which dashboard this widget is suitable via requiresContexts (matches providesContexts from the dashboard) and for (matches typeof the dashboard).
The type definition provides two generic parameters TConfig and TConfigState which both default to undefined. If state should be included the second generic parameter MUST be provided, otherwise the state will be considered undefined.
Note: If a widget's title should get translated, it must further export a constant messages and must use the id of the message as it's title. The message must contain at least 1 dot (.) and should have a form like widget.title.WidgetTile. Afterwards the tasks yarn i18n:extract / i18n:compile must be run.
-
index.ts: Should reexport the Widget and the definition.
-
Register widget
After a widget is defined in the previous step it has to be registered in client/components/widgets/widgets.ts. This module imports the definitions of all widgets and defines 2 things:
- function
widgetResolver which resolves the widget's componentName prop to a react-component.
- hook
useAvailableWidgets which should respond with all available widgets for the catalog (Note: order of widgets is preserved).
Dashboard Definition
In order to create a dashboard the following steps have to be performed:
-
create a default-dashboard (see server/seed/index.ts): A default dashboard is like a generic version that users may be able to customize to their liking. A dashboard here is an object of type Dashboard. There are many configuration options available, the most important ones are:
- id: a unique id which identifies this dashboard
- columns: the number of columns of the dashboard. If this is set to 12 that basically means that the horizontal space is devided into 12 equal sections and a widget can choose to take up anywhere from 1 to 12 units.
- gap/rowHeight: the gap and the rowHeight for widgets. Gap is like a gutter between the widgets. Gap and rowHeight define the virtual height unit of a widget. Note: 8 should be a good starting point for each.
- isEditable: boolean that defines whether or not a user can change the dashboard.
Note: if a widget wants to resize itself (e.g. to prevent some overflow) the dashboard has to be editable!
- type: an enum
DashboardType that helps specify different kinds of dashboards (helpful to limit widgets in the catalog that can be added to a dashboard)
- providesContexts: an array of enum
DashboardDataContext. A dashboard articulates with this prop which contexts will be available for widgets within this dashboard. The idea is, that dashboards should be completely decoupled from the state of the parent component that renders the dashboard and also must not depend on the router setup. So it merely defines that this information will be defined by whomever renders the dashboard.
- widgets: an array of widgets
- layouts: an object that specifies the layout for the 3 breakpoints that are supported. Each layout-item refers to the widget via it's
i property. See next section on how to create layouts.
It's important to understand that a user should only be able to add certain widgets to a dashboard that actually work and make sense. Therefore useAvailableWidgets() only returns widgets that:
- match the column definition
- match the type of the dashboard
- requires contexts that are provided by the dashboard
- are not restricted by permissions and/or features
-
Render the dashboard via the DashboardProvider/Dashboard component
<DashboardProvider
id={dashboard.id}
userName={user?.userName}
widgetResolver={widgetResolver}
dataContexts={dataContexts}
dataContextProps={{
[DashboardDataContext.EnergyCenter]: {
energyCenterId: Number(energyCenterId),
},
}}
isEditing={isEditing}
>
<Dashboard autoGenerateLayout />
<DashboardProvider>
The DashboardProvider component takes a couple of props
id: refers to the id of the dashboard that should be rendered.
userName: the user-name of the user
widgetResolver: see above
dataContexts: a map of all possible data-contexts
dataContextProps: values for those data-contexts
isEditing: a boolean flag indicating if the dashboard is in the editing mode
The Dashboard component takes a autoGenerateLayout prop which is options. If true there will always be a layout generated if none is provided via the seed.
Recommended approach to quickly define layouts
Defining the layouts can be a tedious exercise as one has to have a good understanding how the page should look like and how that translates into coords and sizing information in the layout. The recommmended approach is to not build this by hand but to use the app itself to generate the layout information:
-
Define the dashboard with {isEditable: true} first and use the prop autoGenerateLayout on the DashboardRenderer
-
Open the dashboard
-
Bring up the devtools in a separate window, switch on the responsive mode and choose the desired size (one of regular desktop > 1280px, iPad landscape, iPad portrait, 640px, iPhone portrait)
-
Await the refresh (timeout is 3000 millis), enable edit-mode and rearrange the dashboard to your liking
-
Repeat #4 and #5 for all 5 device sizes
-
Query the updated widget- and layouts-config via the graphql-playground.
{
dashboard(id: "asset-dashboard", userName: "105042697") {
id
widgets {
id
title
className
componentName
config
}
layouts
}
}
-
Copy/paste this into the seed and make final tweaks (if any).
-
Optional: If you do not want users to be able to change the dashboard set isEditable to false.
Redis schema migrations
Running migrations
Generally, this is not required for development, as a simple db flush would be sufficient.
NODE_CONFIG_ENV=<<< env-name >>> yarn migrate
Writing migration
Migrations live within the server/migrations folder. To generate a new migration, run
yarn generate:migration hello_world
This will generate a new file within server/migrations with the name format <identifier>_<name>.ts. The identifier is constructed by the current utc time.
The migration file contains a single function named up(version, adapter). This is where your migration code goes. You should handle anything here regarding migrations (e.g. update database entries). If you return a promise, it will be awaited during migration.
To make writing migrations a bit easier, there is a migrationHelper.ts which exports commonly used helper functions.
Testing
Testing is setup with jest and @testing-library/react.
Run with mocks
The app contains mocks for a few apis for testing purposes or for APIs which haven't been implemented yet. Mocks are enabled by default then running tests, but are disabled when using yarn watch command to run the app in development.
However, if you prefer to start the app with mocks you can use the ENABLE_MOCKS=true env variable or just use yarn watch:mocks.
CI/CD
- Base-branches are protected and require PRs
- Tests and static code-checks are automatically run for every PR against the develop-branch and are a prerequisite for merges to the base-branch.
- The deployment is handled by ArgoCD
- Every commit to the develop-, hotfix- or release-branch (patterns:
release/** hotfix/**) is going to be build and deployed to https://energy-management-dev.staging.myplant.io
- Every tag that is pushed (pre-release or release) is going to be build and deployed to https://energy-management.staging.myplant.io
- Every release in created on Github is going to be build and deployed to https://em.myplant.io
Collaboration
-
The project follows the Git-flow-Workflow.
-
Commit messages are standardized. A commit message must have the form:
<type>[optional scope]: <description>
[optional body]
[optional footer]
Few notes:
- the description references the UserStory and a gives a short-description
- All commmit messages may have a body (more text).
- Any breaking change must include a text
BREAKING CHANGE.
- Other commit types as well as scopes are optional.
Here are some examples:
- bugfix:
fix: <US-Story> short description
- new feature:
feat: <US-Story> short description