Architecture of the FreeNAS 10 GUI

The FreeNAS 10 GUI is a React.js webapp served by mach running on node.js. The data flow uses the Flux architecture. For styles, it uses Twitter Bootstrap via React Bootstrap. For a complete list of technologies used in the FreeNAS 10 GUI, see FreeNAS 10 GUI Technologies.

FreeNAS 10 WebApp Architecture

A stacked diagram representing the FreeNAS 10 UI’s “layers”

Layers of the UI

The FreeNAS 10 UI is divided into layers, which map well to Middleware channels. In simplest terms, the “wrapper” (persistent toolbars, titlebar, footer) subscribes to general system information (authentication status, events, alerts, uptime, hostname, etc), and then each view will subscribe to a specific and more detailed stream of information (users, network interfaces, volumes, etc.) for the duration of its open state, and unsubscribe/garbage collect before unmounting.

This creates a modular and highly composible UI, where individual views and layers are only responsble for subscribing to the Flux stores from which they want to receive data, and indicating to the Middleware Client that it should alter a subscription state. It enforces a rigid separation of concerns for data handling as well as conceptually disparate areas of the system.

Main Mount

The entire FreeNAS webapp is rendered by JavaScript. Main Mount is rendered directly into <body, and all other components are descended from it. It is represented by routes.js, which handles the client-side routing, including the FreeNASWebApp component. FreeNASWebApp is the functional “shoebox” wrapper for the application, and subscribes to the general system information channel. It handles authentication, and subscribes to events, alerts, and other notifications.

Each nested route is rendered within its parent, enforcing a strict visual and architectural heirarchy.

Modal Windows

Modal Windows aren’t heavily used in FreeNAS 10, but may be used for things like login, unexpected disconnect, etc. In general, they can receive data from the primary connections managed by the Main Mount.

Notification Bar

The persistent titlebar in the FreeNAS 10 UI also functions as a kind of notification center, handling the events, alerts, and notifications passed into it from the Main Mount’s system-general subscription.

Navigation

The Navigation is currently hard-coded, but in the future, may receive its routes from a web permissions context that doesn’t exist yet. TBD.

The Primary View

The Primary View is generally a React Controller-View which maps to a specific Middleware channel, like “Users” or “Network Interfaces”. This is the primary area of the UI, and is not persistent. For more on how this is handled, pleaes refer to the Flux Architecture documentation.

Contextual Sidebar (Widgets)

The right sidebar of the FreeNAS GUI is a contextual sidebar which listens for the showContextPanel event, and then renders whatever component is passed with the event. When it receives the hideContextPanel event (typically when a component that previously dispatched the showContextPanel unmounts, it ceases rendering.

This section is suitable for use with any view that needs to display additional information side-by-side with a main view, or that only otherwise needs to use the sidebar space. For example, the Storage view uses the sidebar to render a drawer of unused disks. Another possible use would be to provide extended tooltips.

Footer

Nothing to see here at this time.

The Flux Application Architecture

The FreeNAS 10 UI is based on Facebook’s React, a declarative front-end view framework. Because of this, FreeNAS’ UI shares many of the same conventions as a standard React application does.

The Flux Architecture is more of an architectural model than it is a framework or plugin. While it does include a Dispatcher module, Flux is primarily focused on enforcing unidirectional data flow, and accomplishes this through strict separation of concerns.

React does not depend on Flux, and vice versa, but between the two ideologies, it’s very easy to create a highly composible, declarative UI, with straightforward data flow and clear delegation of responsibilities

External Resources:

A high level data flow disgram for the FreeNAS 10 GUI.

Displaying Data

This section is short, since the strict separation of concerns means that React Views and Components display data, and do very little else. However, the primary View is a good touchstone for understanding the rest of the Flux architecture, since this is where user input is entered, and where server output is finally rendered.

React View

A React View can be any React component, but is generally the primary view open in the webapp. While other persistent components (navigation, notifications, widgets etc) function in slightly different ways, they’re broadly similar to a standard React View.

Example of a basic view

Role

A React View displays data to the user, and handles all interactions with the user. It is responsible for maintaining internal state, and updating itself when newer data is available.

Input

In the above screenshot, the Groups view is open. Following the diagram at the top of this guide, the Groups view receives new information from the Groups Flux Store. The view does not modify the Flux store, and has no opinions of its contents. When a React View is first initialized, it will often subscribe to an empty Flux store, and display nothing. In a few moments, when the Flux store is updated with the relevant data, the React View will re-render itself to display that data.

Output

The React View submits events, data, and requests to the Middleware Utility Class. In the example of the Groups View, if a group is edited - for example, its name is changed - upon saving, the updated group object is sent to the Groups Middleware Utility Class. The React View is ignorant of what will then happen to the group, and does not register a callback or perform any followup actions. When the group is updated, or an error occurs, it will be communicated through the same subscription to the Flux Store described above.

Submitting User Input

The next step in the Flux Architecture is handling user input and sending it to the server. There is a deliberate pattern which will emerge, in which each “step” is ignorant of what came before it, and is only responsible for taking the data given to it and performing the appropriate next step.

Middleware Utility Class

The Middleware Utility Class (MUC) is an abstaction provided by FreeNAS 10, and while suggested by Flux, it isn’t a strict requirement of the architecture. It provides an interface between the React View, the Middleware Client, and the Action Creators. When a user interacts with the FreeNAS 10 UI in a way that will require the Middleware Server to provide new data, the action is handled by the MUC, which calls the Middleware Client’s request() method with a callback for the appropriate Action Creator.

Role

The MUC pipes request data into a public method provided by the Middleware Client, and registers a callback that will be run when a matching response is receieved from the Middleware Server. The MUC does not modify input data, and does not manipulate response data.

The ambiguation provided by this class is necessary for a few reasons:

Consistency

Because the MUC exists outside of a React View’s lifecycle, it is able to guarantee that the registered callback will be run even if the original requesting View has closed.

Concurrency

Because the MUC is a singleton, it is also concurrently available to other views while retaining internal state.

Flexibility

Because the MUC is not combined with an ActionCreator, it is more composible, and may contain methods which register callbacks tied to many different ActionCreators.

In this way, the architecture ensures that no replies are regarded as spurious by views which should have no knowledge of them, and the entire application maintains consistent state.

Input

The MUC recieves raw event data, objects, and other pre-packaged interactions from a React View. These might be as simple as a click event, or as complex as a dictionary object representing the changed properties for an array of hard disks. The MUC is deliberately ignorant of the Views which send it data.

Output

The MUC registers a callback with one of the Middleware Client’s public methods, ensuring that once the Middleware Client has recieved a response from the Middleware Server, the response data is passed into the callback. The callback is almost always a public method exposed by an ActionCreator class, which will handle the response data.

Middleware Client

The FreeNAS 10 UI uses a fully asyncronous WebSocket connection for communication with the hardware backend. The Middleware Client is a simple WebSocket client which handles the connection lifecycle, as well as the packaging, identification, transmission, and (initial) receipt of data.

Role

The Middleware Client exposes public methods for connecting/disconnecting, logging in/out, subscribing/unsubscribing from event types, and making specific requests to the Middleware Server. It can be thought of as a sort of companion to the FreeNAS 10 Command Line Interface, as it provides similar functionality.

Dangers

The Middleware Client should not be accessed directly from a View.

Directly accessing the Middleware Client can cause data to be returned and not handled, or treated as a spurious reply with no known origin.

The Middleware Client does not and should not modify Flux Stores or Views.

Input

The Middleware Client exposes functions like request(), which are meant to be called from a Middleware Utility Class. These methods should be provided input data to send to the Middleware Server, and also provided at least on registered callback to a method exposed by an ActionCreator. The first callback provided will be used to handle responses that indicate success, and the second will be used to handle any errors.

Output

The relevant registered callback to the ActionCreator will be run when an appropriate response is received from the Middleware Server, and the callback function will take the response as its parameters.

On the Server

This part of the guide is only provided to give a more complete understanding of the system as a whole. No GUI developer should ever need to worry about the specifics of the Middleware Server, or even the underlying FreeNAS OS. It may as well be a black box which receives packaged calls and returns new data.

Once loaded, the GUI is even capable of operation in the complete absence of a FreeNAS Server. This mode is largely intended for implementing GUI behavior while the relevant server functionality is unavailable.

Middleware Server

The Middleware Server is a WebSocket server running on the same hardware as the core FreeNAS 10 OS. It collects and disburses system data requested by a Middleware Client. It is capable of handling event queues and multiple (non-blocking) requests. It can connect to many clients at the same time, and correctly triage requests and responses to each, concurrently.

FreeNAS 10 Base OS

The core operating system. Out of scope for any UI work, and shown in the above diagram only to describe its relationship to the rest of the system and position in the flow of logic.

Handling Data from the Middleware

After being sent a request, the Middleware Client will receive a response from the Middleware Server. This isn’t necessarily a 1:1 relationship, as a subscription request will cause the Middleware Server to send a stream of “patch” responses to the Middleware Client. Fortunately, the ActionCreators and other Flux errata are ignorant of their data’s sources, and only care about how to process it and where to send it.

Action Creators

Action Creators aren’t provided or created by Flux, but they are a necessary abstraction when piping multiple data streams into the same Dispatcher.

While conceptually simple, an Action Creator class is an easy way to group similar functions, and attach identifying information to the packaged data. It limits code reuse, and creates a clear channel for handling data from the middleware. Flux suggests ActionCreator classes as an alternative to putting all of the processing functions inside the Dispatcher itself - reducing the size and complexity of the Dispatcher module and allowing for a more visible separation of concerns.

This also allows for simpler debugging, and creates a more extensible and composible platform than just calling FreeNASDispatcher.dispatch() directly would.

Role

Action Creators handle response data from the middleware, process and tag it as necessary, and call the appropriate method within the Dispatcher to perform the next step.

Input

ActionCreator methods are registered as callbacks by Middleware Utility Classes, and are called by the Middleware Client when a response is given for the original request. The response data is passed into the ActionCreator function, where it is packaged, tagged, and processed (if necessary).

Output

ActionCreator methods call shared methods within the Dispatcher, and send them the payload data. It’s the responsibility of these methods to identify the source of their payload, so the ActionCreator needs only to select the correct handler in the Dispatcher.

Flux Dispatcher

The Dispatcher broadcasts payloads to registered callbacks. Essentially, a store will register with the Dispatcher, indicating that it wants to run a callback when the Dispatcher broadcasts data of a certain “action type”.

Role

The Dispatcher is only responsible for broadcasting data to registered callbacks (Flux Stores). It contains handler functions that will tag the payload with a source, and these are selected by the ActionCreator. Primarily, they assist with debugging, and are a final opportunity to perform processing or tagging on the payload before it is broadcast to the Stores.

Input

Handler functions which ultimately call FreeNASDispatcher.dispatch() are registered in ActionCreators, and are called whenever the ActionCreator is receiving data.

Output

Whenever FreeNASDispatcher.dispatch() is called (usually by a handler function), the data parameter is broadcast to registered callbacks (Flux Stores)

Callbacks are not subscribed to particular events. Each payload is dispatched to all registered callbacks, and it’s up to the callback to triage the action type, and decide whether to act.

Flux Stores

A Flux store is, at its core, a simple JavaScript object. Stores are exported as singletons, so each store is both a single instance and globally accessible by any other module or view.

Role

Flux Stores are persistent data stores, accessible by any view or component. They can be relied on to always have up-to-date information from the Middleware, and obviate the need to perform long-polling operations.

Stores additionally function as event emitters, and allow views to “subscribe” to the store’s “change” event, and register a callback to be run when the store is updated.

In this way, data upkeep and processing tasks are abstracted out of the view, and the view can rely on always having up-to-date data provided automatically by the store.

Stores also tend to have utility functions for retrieving specific data.

Input

Stores are only ever modified by the Dispatcher. They receive every broadcast payload the Dispatcher ever sends out, and will generally have a switch function that determines whether the broadcast is applicable to the type of data that the Store is concerned with. This determination is usually based on the action type added by the ActionCreator.

Output

Each React View will choose to subscribe to events emit by a specific Flux store, and additionally may request some or all of its data at various points in its lifecycle. When the Flux store updates, it will emit an event, causing the Change Listeners registered with that store to execute. Usually these will be functions to re-request the data updated in the store (which may cause the component to re-render to display the update).

The Flux Store is ignorant of which views are subscribed to it, and persists as a singleton outside the lifecycle of any View or Component. In this way, it is always up to date, and can act as a single source of truth to multiple Components in parallel.

Routing

FreeNAS 10 uses react-router to provide client-side routing. react-router is not officially part of React, but it shares many developers and uses avant-garde React features. As of this writing, the public docs for react-router are at rackt.github.io/react-router.

Because the FreeNAS 10 GUI uses client-side routing, the page is never refreshed or changed during a session. One of the interesting effects of this is that as the visible React components are changed or selected, the route in the browser bar changes to reflect that.

There is far more to react-router than just the following. We strongly encourage all FreeNAS developers to become familiar with react-router in depth.

A simple route looks like this:

            <Route
              name    = "groups"
              path    = "groups"
              handler = { Groups } />
          

The name is used to identify the route internally. The path is the string that will appear in the browser when that path is active. Finally, handler is the component that will be rendered with router-specific props and lifecycle functions. For example, when you navigate to myfreenas.local/Accounts/Groups, the Router renders a RouteHandler which renders the Groups component with the extra router props and lifecycle functions.

Do not add a route without a working handler. Attempting to load a Route with an invalid RouteHandler will result in an Internal Server Error.

Routes can be nested inside another Route. groups itself is nested inside the accounts route, which in turn is nested inside the root route, /. In the example below, add-group is nested inside groups. This creates the route myfreenas.local/accounts/groups/add-group. Static nested routes like this are used when the name of a route will never need to change. Routes for the main parts of the UI are all static, as are the routes to add new entities.

            <Route
              name    = "groups"
              path    = "groups"
              handler = { Groups }>
              <Route
                name    = "add-group"
                path    = "add-group"
                handler = { AddGroup } />
            </Route>
          

Dynamic Routing

The dynamic portion of a path has the form :paramname. A Link to a dynamic path has a params prop that contains the approprate object to be used as the dynamic part of the path. For example, a Link to the wheel group would look like <Link to = "groups-editor" params = { groupID: "wheel" }> {visualcontent}</Link>. The path upon clicking that link would be myfreenas.local/accounts/groups/wheel. The Route nesting used to produce this behavior is as follows:

            <Route
              name    = "groups"
              path    = "groups"
              handler = { Groups }>
              <Route
                name    = "add-group"
                path    = "add-group"
                handler = { AddGroup } />
              <Route
                name    = "groups-editor"
                path    = "/groups/:groupID"
                handler = { GroupsItem } />
            </Route>
          

The Link above loads the groups-editor path with wheel as the groupID param. When the RouteHandler (in this case, GroupsItem) is rendered, is has access to the param and the active route, and is able in turn to render the correct item. This works even if the item was loaded directly from the URL.

Any static route must be nested before the dynamic route, because otherwise the dynamic route will attempt to pass the param to its handler and the intended static route will fail to load. This also means in this case that a group called “add-group” will collide with the static route and fail to load if linked directly from a URL.

Routing and the Viewer

Part of the functionality of the the Viewer Component is the ability to create dynamic routes based on the visible item. For example, when you click on wheel in the Groups DetailViewer, the URL displayed in the browser bar changes to myfreenas.local/accounts/groups/wheel and the item view for wheel displays.

Viewer Routing Props

The Viewer uses props of its own to render the correct route.

FreeNAS 10 GUI Technologies

React.js

React is a JavaScript library for creating user interfaces. It is unlike MVC frameworks (eg. Ember, Backbone, Angular) as React aims only to provide self-updating, dynamic views. React uses a virtual DOM and hashes changes to the in-browser DOM, so its event-system, templates, and supported features are properly represented across all browsers, regardless of age.

React is rendered serverside in FreeNAS 10, so that the initial payload sent to the user contains the HTML output of the React template, the virtual DOM is preloaded, and the component’s state is already initialized.

Because React focuses on creating “components” instead of “pages”, it also works well with Node and Browserify‘s require() syntax to keep files short, legible, and well organized. Components require() each other, creating a visible nested heirarchy.

Developers who are familiar with writing static HTML pages should be quickly familiar with React’s pseudo-HTML syntax (JSX), which provides both a gentle learning curve and valid semantic abstractions for the JavaScript it represents.

Twitter Bootstrap

Twitter Bootstrap (TWBS) is one of the most popular and well-known HTML, CSS, and JavaScript frameworks in use today. It has been widely adopted in a variety of websites, from personal blogs to Fortune 500 companies.

The act of switching away from the toolkits used in previous versions of FreeNAS has given the project more freedom, but also introduced a lack of visible structure. Primarily, the use of TWBS in FreeNAS 10 leverages a recognizable, well-documented platform with a shallow learning curve, and promotes the use of pre-existing patterns to design and organize content.

But what about LESS?

TWBS is used in FreeNAS 10 in a slightly unconventional way. Rather than using the pre-packaged download, the LESS source files for TWBS are compiled at build time, together with the FreeNAS 10 LESS files, to create a single master stylesheet.

Doesn’t TWBS require jQuery?

The JavaScript components are not included verbatim, but rather provided by React Bootstrap, a companion library that includes simple React reimplementations of the TWBS components without jQuery.

LESS

LESS is a CSS-like language which compiles to CSS. It features variables, mixins, imports, and heirarchical class declarations which make development simpler. LESS can also be split into several different files for better organization and separation of concerns.

LESS is used in FreeNAS 10 primarily for its utility, and because Twitter Bootstrap is based on LESS. Compiling from LESS creates a single, unified file with fewer overwrites or complicated rules. The mixin architecture allows for powerful and dynamic expressions, as well as a simpler development process. It also means that the Twitter Bootstrap variables and mixins can be redefined or modified in our own LESS files, without modifying contents of the originals.

Velocity

Velocity is a ground-up reimplementation of jQuery’s $.animate() function. It is lightweight, and more consistently more performant than jQuery. It handles JavaScript-based UI animation, queues, and other transitions.

D3

D3 (short for Data Driven Documents) is a JavaScript library for manipulating documents based on data. It is capable of providing rich visualization in the form of charts, graphs, maps, and more. In particular, it’s used for FreeNAS 10’s system overview, providing realtime graphs of CPU, network, disk, etc.