Developing the FreeNAS 10 GUI

Setting up your Development Environment

FreeNAS 10 development is currently supported on FreeBSD, Mac OS X, and some Linux distributions. Windows users might be able to get it working, but it's not and will not be an officially supported platform for development.

If you already have node.js installed and the repo checked out, great! Skip to "Using the FreeNAS 10 Development Environment" below.

Otherwise, to begin developing for the FreeNAS 10 GUI on one of these platforms, simply do:

curl -o- https://raw.githubusercontent.com/freenas/gui/master/bootstrap.sh | sh

This will analyze your development environment and bootstrap the development toolchain onto it as necessary, also checking out a working copy of this git repository for you. It will also install node and npm if they aren't already available.

Alternatively, if you have already cloned this repo then just do

sh bootstrap.sh

from the root of it to accomplish the same thing.

Using the FreeNAS 10 Development Environment

If you used the bootstrap script, you'll already be ready to run 'gulp'. If not, run

npm install -g bower gulp forever jshint jscs esprima-fb@15001.1.0-dev-harmony-fb

and npm install from the root of the repo first.

Once your development environment is initialized, run 'gulp' to start the FreeNAS 10 SDK app.

You will need to choose whether to target a real FreeNAS instance in order to interact with the middleware, or to operate in "dumb mode", in which case it will run a local copy of the GUI webapp that simulates interaction with real data. 'gulp --connect FreenasIPorHostname' will start you in live development mode with a real connection. 'gulp' alone will start you with a local webserver only, so you can work on UI elements for as-yet-unavailable middleware functionality.

Once the app is running, it will monitor your source files and automatically rebuild and restart the GUI every time a file changes.

Working on FreeNAS

This section will explain how to implement a complete FreeNAS 10 GUI section. It will rely heavily on the FreeNAS 10 GUI Architecture documentation. We strongly suggest you read that first. We will also assume you are already familiar with React and JSX.

Code for the actual FreeNAS webapp resides in the app/src/scripts directory. We will be using the Accounts view for this guide. It is one of the primary views, and it touches on nearly every part of the FreeNAS GUI codebase.

A New View

The main sections of the FreeNAS UI are the views, reachable from the primary navigation sidebar on the left side of the webapp. Accounts is one of the primary views, providing access to all the users and groups on the system.

The Directory Hierarchy

The main JSX file for a given view resides in the app/src/scripts/react/views directory. Any related sub-components are placed in a corresponding sub-directory within app/src/scripts/react/views. This same pattern holds all the way down. For example, Users.jsx and Groups.jx can be within the app/src/scripts/react/views/Accounts directory, and GroupItem.jsx, GroupEdit.jsx, GroupView.jsx, and GroupAdd.jsx can be found within app/src/scripts/react/views/Accounts/Groups. This mirrors the route nesting in routes.js.

The Main View File

Accounts.jsx is primarily just a wrapper for its subsections, Users and Groups. It provides the tabbed navigation structure at the top of the view using the SectionNav component. Accounts just renders the SectionNav and then the RouteHandler determined by the active route, which can be either /accounts/users or /accouts/groups. For more on routing in the FreeNAS 10 GUI, see the Routing section of the architecture docs.

Next we'll look at a nontrivial component, Groups.

The Middleware Cycle

Groups is a component that manages the display of system- and user- created groups. It uses the Viewer component to construct various different modes of displaying the data, but its main role is managing the live data for display to the end user. This places it at the React View position in the Flux data lifecycle. We will use Groups as an example to demonstrate the Flux data lifecycle in FreeNAS 10 in detail.

The React View

Any component that wants to acquire data from the middleware must use a number of standard lifecycle functions. During componentDidMount, it must:

  1. Set a change listener to each store that will hold data it needs.
  2. Request initial population of those stores, in case it is the first componenent mounted to require that data.
  3. Subscribe with the middleware to future changes to that data.

Correspondingly, during componentWillUnmount, it must:

  1. Remove all change listeners it set.
  2. Unsubscribe from all data sources.

In Groups, this looks like:

            , componentDidMount: function () {
              GS.addChangeListener( this.handleGroupsChange );
              GM.requestGroupsList();
              GM.subscribe( this.constructor.displayName );

              US.addChangeListener( this.handleUsersChange );
              UM.requestUsersList();
              UM.subscribe( tthis.constructor.displayName );
            }

            , componentWillUnmount: function () {
              GS.removeChangeListener( this.handleGroupsChange );
              GM.unsubscribe( this.constructor.displayName );

              US.removeChangeListener( this.handleUsersChange );
              UM.unsubscribe( this.constructor.displayName );
            }
          

This is straightforward, but any missing parts of this process will result in either missing steps in the data lifecycle or broken app behavior. Groups also defines some simple functions to use for populating state and as changeListeners.

The functions being called in the lifecycle functions above come from two main groups: The Middleware Utility Classes and the Flux Stores. We'll look at a middleware utility class next, as that's the next step in the flux data lifecycle.

The Middleware Utility Class

As described in the Middleware Utility Class section of the architecture documentation, a middleware utility class provides an abstraction for all calls to the middleware client. Middleware utility classes should generally be focused on a particular namespace in the middleware. In this case, it's groups. All middleware utility classes should live in the TBD3 directory and be named according to the namespace the cover.

As FreeNAS GUI development proceeds against features without corresponding middleware functionality, middleware utility classes will also need to be adapted to TBD HOW WE HANDLE THE NEW LIFECYCLE

All middleware utility classes reside in app/src/scripts/flux/middleware .

The basic functions nearly any middleware utility class will need are subscribe, unsubscribe, and at least one request function. These functions simply wrap common middleware calls. subscribe and unsubscribe also take a string as the name of the function to subscribe. This is used only for recordkeeping and debugging - it does not change the behavior of any responses. GroupsMiddleware implements these functions as follows:

            static subscribe ( componentID ) {
              MC.subscribe( [ "groups.changed" ], componentID );
              MC.subscribe( [ "task.*" ], componentID );
            }

            static unsubscribe ( componentID ) {
              MC.unsubscribe( [ "groups.changed" ], componentID );
              MC.unsubscribe( [ "task.*" ], componentID );
            }

            static requestGroupsList () {
              MC.request( "groups.query"
                        , []
                        , GAC.receiveGroupsList
                        );
            }
          

The rest of the functions in a middleware utility class wrap other middleware calls as necessary. Functions that anticipate a response from the server (including the request function above) should pass an appropriate action creator function as a callback. See the next section for an example of an action creator.

            static createGroup ( newGroupProps ) {
              MC.request( "task.submit"
                        , [ "groups.create" , [ newGroupProps ] ]
                        , GAC.receiveGroupUpdateTask
                        );
            }

            static updateGroup ( groupID, props ) {
              MC.request( "task.submit"
                        , [ "groups.update", [ groupID, props ] ]
                        , GAC.receiveGroupUpdateTask.bind( GAC, groupID )
                        );
            }

            static deleteGroup ( groupID ) {
              MC.request( "task.submit"
                        , [ "groups.delete", [ groupID ] ]
                        , GAC.receiveGroupUpdateTask.bind( GAC, groupID )
                        );
            }
          

The Action Creator

As described in the Action Creators section of the architecture documentation, the action creator packages responses from the middleware using pre-defined event types, and then calls FreeNASDispatcher.handleMiddlewareAction(). This helps Flux Stores to identify relevant middleware events and respond appropriately, without overloading the Dispatcher to both tag and dispatch events.

All action creators reside in app/src/scripts/flux/actions .

Most action creators will be very short - they simply have a function for each type of event they anticipate receiving from the middleware and tag each event accordingly.

            static receiveGroupsList ( groupsList, timestamp ) {
              FreeNASDispatcher.handleMiddlewareAction(
                { type: ActionTypes.RECEIVE_GROUPS_LIST
                , timestamp
                , groupsList
                }
              );
            }

            static receiveGroupUpdateTask ( groupID, taskID, timestamp ) {
              FreeNASDispatcher.handleMiddlewareAction(
                { type: ActionTypes.RECEIVE_GROUP_UPDATE_TASK
                , timestamp
                , taskID
                , groupID
                }
              );
            }
          

Action creator functions should only be passed as callbacks by middleware utility classes anticipating responses of that type. Once an event is received and tagged, it is passed to the Dispatcher, which in turn broadcasts it to all Flux stores.

FreeNAS Constants

Action types for ActionCreators are defined in FreeNASConstants . A quick side trip there produces the following:

              // Groups
            , RECEIVE_GROUPS_LIST: null
            , RECEIVE_GROUP_UPDATE_TASK: null
          

FreeNAS 10 GUI Style Guide

FreeNAS 10 GUI uses a strict style guide and a highly encouraged set of other best practices for all JavaScript and JSX code. The goal of these rules is to make contibuting to FreeNAS 10 easier, by reducing the need to grasp and duplicate multiple code styles in different parts of the codebase. Additionally, several of the rules are aimed toward increasing readability and making simple errors immediately obvious.

The tool used for testing for and displaying style guide deviations is JSCS. For the exact JSCS rules used in FreeNAS 10, look at the .jscsrc file in the root of the FreeNAS 10 source code.

The FreeNAS 10 Frontend Development Environment includes a tool for automatically checking compliance with all style guide rules. When a GUI rebuild is triggered, it will scan the entire codebase and print all the style guide violations it detects. Eventually the tool will bail out and refuse to update the GUI if it detects any style guide violations at all. There are also JSCS Plugins for popular editors that show JavaScript errors, warnings, and style guide deviations live as you develop.

JavaScript Code Rules

Our style rules are based largely on the rules from the node style guide , with selected rules added from the npm style guide . This guide is largely adapted from those two sources. Accordingly, this style guide is licenced under CC-BY-SA.

80-column Lines

This is typically for consideration to those using terminal editors with limited space. However, the real reason we’re requiring this is to force you to keep your code relatively concise. If you find yourself in need of 120 characters to express a single statement, consider rewriting it. A number of other rules in this guide will help with keeping your lines short.

Opening Braces on the Same Line

When you use a keyword that requires a bracket to contain the subsequent statement, put the opening brace of that statement on the same line as the keyword.

            let goodTimes = true;
            if ( goodTimes ) { // Good
              console.log( "That's the way uh huh uh huh we like it." );
            } else if ( "pls no" ) // Bad
            {
              console.log( "KNF has its place, but it is not here." );
            }
          

Method Chaining

If you must chain method after method, put one on each line, leading with the dot. This is your way around the 80-character limit when you really need that long statement.

            User
              .findOne({ name: 'foo' })
              .populate( "bar" )
              .exec( function( err, user ) {
                return true;
            });
          

One Variable per Statement

Declaring a bunch of variables on one line may be appealing, but it makes finding where a variable is declared annoying and can easily lead to simple mistakes (like missing commas). Type var, const, or let once per variable.

            // Good:
            let foo;
            let bar;
            let baz;

            //Bad:
            let
              foo
              , bar
              , baz;

            // Very Bad:
            let foo, bar. baz; //oops!
          

Use ===

Fuzzy comparisons result in fuzzy bugs. Use ===, or lodash isEqual, rather than ==. != is right out.

Comma First in Multi-Line Lists

This is a bit different. Basically, when you’re listing a bunch of things on multiple lines, each line should start with a comma except the first one. This lets you line up all the commas under the opening brace or bracket, as a bonus.

The chief benefit of this is that it’s immediately obvious when you’ve forgotten to put a comma between two items. It also makes those long arrays and objects much easier to read.

            // Good:
            let bestArray = [ foo
                            , bar
                            , baz
                            ];

            // Bad:
            let badArray = [ foo,
                             bar,
                             baz ];

            // Very Bad:
            let uncoolObject { foo: "Don't", bar: "do" baz: "this" };
            // Broke it again!
          

Two Space Intentation

All GUI code must use two-space indentation. Not two-space tabs - two spaces. On the bright side, that will give you some extra space to work with compared to 4-space or 8-space tabs, because we also use 80-column lines.

No Trailing Whitespace

Whitespace at the end of a line has no reason to exist. This also means that when a line is just a newline, there shouldn’t be any spaces or tabs in it.

Space Before Parentheses

For just about any keyword that is followed by a parenthesized statement, put a single space before the opening parenthesis. Function calls are once case where you should not use a space before a parenthesis.

            let youDoTheGoodThing = true;
            let youDoTheBadThing = { please: "don't" };

            if ( youDoTheGoodThing ) {
              console.log( "Everyone will be happy!" );
            } else if( youDoTheBadThing ){
              console.log( "Everyone, especially you, will be sad when your "
                         + "code is full of warnings." );
            }
          

Spaces Inside Brackets and Braces

Don’t press your braces, brackets, and parentheses up against their contents. The only exception is when it’s an array or object and the very next character is another brace or bracket. This is mostly for readability.

            // Good:
            let floor = { room: "for activities" };

            let hardwareStore = [ "look"
                                , "at"
                                , "all"
                                , "this"
                                , "stuff" ];

            // Bad:
            let iStealPets = [{ I: "have"
                              , so: "many" }
                             , { friends: null }];

            let musicalChairs = ["the" // eww, it doesn't line up
                                , "music"
                                , "stops"];
          

Spaces Inside Parentheses

Whenever you have parentheses around something, put spaces between each parenthesis and what it contains.

            let youWantToDoItRight = true;
            let youDontWantToDoItRight = "WHY?";

            if ( youWantToDoItRight ) {
              console.log( "You'll do it like this:"
                         , { haha: I'm printing an object" }
                         );
              console.log( [ "check"
                           , "out"
                           , "this"
                           , "array"
                           ]
                         );
            } else if (youDontWantToDoItRight) {
              console.log("Oh Man I Am Not Good With Computer"
                         , [ "pls"
                           , "to"
                           , "help" ]);
            }
          

JSX Code Rules

These rules apply to JSX-specific syntax. These rules are NOT automatically enforced by JSCS, and are not yet implemented throughout the UI. However, they will still help with readability and reasonable line lengths, so they may be considered strongly recommended best practices. As a matter of convention, any file that includes JSX should have the .jsx extension and reside in an appropriate place within the scripts directory.

One Prop per Line

When rendering a Component with more than one prop, put each prop on a new line. The props should be indented two spaces, measuring from the “<” that starts the Component.

            render: function () {
              // ...
              return (
                <TWBS.Grid fluid> // One prop, one line.
                  // ...
                  <TWBS.Row>
                    TWBS.Col // Many props, many lines.
                      xs={3}
                      className="text-center">
                      <viewerUtil.ItemIcon
                        primaryString = { this.props.item["name"] }
                        fallbackString = { this.props.item["id"] }
                        seedNumber = { this.props.item["id"] } />
                    </TWBS.Col>
                  </TWBS.Row>
                </TWBS.Grid>
              );
            }
          

JSCS Plugins

There are JSCS plugins for a number of popular editors. This guide will cover only editors known to be in popular use among FreeNAS 10 developers.

For a list of other plugins and tools, see the JSCS website .

SublimeText

TODO

vim

This was done on PC-BSD 10.1. The process for installing and configuring Syntastic may differ on your OS or distribution of choice. It assumes you have node and npm installed already.

  1. sudo npm install -g jscs
  2. sudo npm install -g esprima-fb
  3. cd ~/.vim
  4. mkdir bundle
  5. mkdir plugin
  6. mkdir autoload
  7. curl -LSso ~/.vim/autoload/pathogen.vim https://tpo.pe/pathogen.vim
  8. cd ~/.vim/bundle
  9. git clone https://github.com/scrooloose/syntastic.git
  10. Edit ~/.vimrc and add these lines to the end of it (copy the default one over from /usr/local/share/vim/vim74/vimrc_example.vim if you don't already have one):
            call pathogen#infect()

            set statusline+=%#warningmsg#
            set statusline+=%{SyntasticStatuslineFlag()}
            set statusline+=%*

            let g:syntastic_always_populate_loc_list = 1
            let g:syntastic_auto_loc_list = 1
            let g:syntastic_check_on_open = 1
            let g:syntastic_check_on_wq = 0
            autocmd FileType javascript let b:syntastic_checkers = findfil('.jscsrc', '.;') != '' ? ['jscs'] : ['jshint']
          

This configuration will make JSCS work so long as you open files from within a terminal in the FreeNAS build directory. If you want it to work a little more universally (i.e. opening files in gVim from a file manager) you can create a symbolic link from your home directory to the .jscsrc in your FreeNAS source directory.

For more information on the Syntastic vim plugin please visit their GitHub page: Syntastic GitHub

emacs

Not yet written. Feel free to take this!

atom

Not yet written. Feel free to take this!