We just released the Nexosis dashboard as open-source. Zach explains how we did it and things we learned along the way.
We have built a single-page application (SPA) application using Elm as a front end for our machine learning API. We have released the dashboard as an open-source project, and will be continuing to work on it in Github. This is both as a form of open documentation on how to use our API product, and also, as a larger scale example of a real production Elm application.
At the time of this writing, the codebase is a little under 15k SLOC in Elm. Most of the code is Elm. The only JS used is initializing the app and some handling of ports. Since this was a greenfield project, we did not do any transitions from another JS framework. There were a few bits of Elm copied over from other another small project, but not much.
The first commit was on 01/22/2018, and we released the first version on 03/28/2018. There were 4 developers working on the project over this time period, though not necessarily full time. If you would like a little more background about the actual processes of working with Elm, my co-worker Jeff has written a blog post on this topic.
The build system uses Webpack, and is a trimmed down and adapted version of coryhouse/react-slingshot. This was mostly copied from another internal project, which builds a hybrid React and Elm codebase. Other options were attempted, but, in the interest of time, we used the familiar system which had been proven to work.
The Elm codebase is actually split between two repos. The actual SPA application and the API client. After the initial release, we extracted the code which makes calls to our API, put this into a separate repo, and published it as a standalone Elm package. This allows the client interfacing code to be used by other applications. This processing was mostly cutting code from the
Data folders, and them moving them to a different repo.
Aside from this difference, the layout is mostly the same as the template we started with. Page, View, Data, Request.
Probably some of the more interesting things to look at from an outside perspective would be the View components that we built. Some of these do have analogues published as standalone packages, but we did not opt to use these all of the time. For example, the
Pager control was copied from another project, and already looked the way we wanted it to.
Probably some of the more interesting things to look at from an outside perspective would be the View components that we built. Some of these do have analogues published as standalone packages, but we did not opt to use these all of the time.
There are a few packages which provide a
Dialog control, but, I knew we would want some specific CSS styling which would probably be harder to add to other controls, but trivial if we wrote all of the code ourselves. There is a package for showing a tabbed interface, but, we didn't even extract that as a reusable module. It probably could be, but, we only used the tab interface in a few spots, and the overhead of building this into the page isn't that high.
Most of these controls started out being completely embedded within whatever page they were needed for first, and then extracted as something standalone as needed. It is easier to write a small bit of Elm for a specific use case, and then generalize it later on if you really need to do so. That also seems to hold for small simple control packages, especially when it comes to styling.
It is easier to write a small bit of Elm for a specific use case, and then generalize it later on if you really need to do so. That also seems to hold for small simple control packages, especially when it comes to styling.
Some controls stayed very simple. A few that got progressively more complicated were the
Grid, which started out as the elm-sortable-table. At first,
Grid was a wrapper over
Table with some project specific default configurations. But, once we progressed to doing server side sorting, the original
Table implementation certainly didn't fit that use case, so the code from the package was pulled into our project and modified to support this new behavior.
Another fairly complicated control is the
Wizard. Steps are specified, along with validations that should be performed on each step. The last "complete" action of the wizard then runs a full validation of the
model, produces a
Msg with a payload of some kind which is then carried out on the next update.
Also, after V1 had been released, we started using some custom elements. The elm-vega package in particular was very suited for use with a custom element. Our initial approach was to build up the Vega spec with the elm-vega package, and then send that out through a port, which would render the spec within a node in the view. This was fine for a single chart, but then became a bit of a struggle when rendering the histogram charts in table rows.
Dashboard histograms in the "Distribution" column
Some of the rows may be filtered, or on a different page and not shown. Our first pass was generating all of the histogram specs in the
update function, but ignoring most of them once the render occurs, since there may only be 10 items displayed on the page. Using a custom element really helped clean this up and allowed us to use the same method of just rendering the current
model in the
view function, without needing to worry about calculating what would be shown twice.
I had not seen a full example which handled pollyfills and custom elements within Elm, so, hopefully this codebase clearly shows an approach that can be used to wire this up.
In all, this is a very enjoyable codebase to work on. There are a few things that can surely be better, but, we are continuing to add new features, and will keep improving things as we go. We also welcome any suggestions or contributions that the Elm community may provide. Also, I want to thank everyone who has contributed code, blog posts, or knowledge that let us build this. We certainly wouldn't have been able to do this in such a tight time-frame without a great language and great resources.