AngularJS admin application for DreamFactory v2.0 and up.
Administer your DreamFactory instance from anywhere with this admin application. Customize with themes from Bootswatch or roll your own with SCSS/SASS. Concat, minify, and uglify component modules with Node.js, Grunt, and the included grunt script for a deployment ready application.
Clone the repo. Navigate to the top level directory of where you cloned the repo and type bower install
. NOTE: you must have Node, Grunt, and GruntCLI installed.
The app comes prepackaged with a grunt file that concats, minifies, uglifies, compresses, and reorgs the source files into a more "transit" friendly manner. It decreases load time and automatically busts client side caches so your changes will be seen the next time a client uses it without any pesky manual cache clearing. This process will create a folder named dist
that will contain the app after processing. From here on out the phrase 'build the app' is referring to this process. To run the build process simply type grunt build
on the command line whilst in the top level directory of the app. NOTE: you must have Node, Grunt, and GruntCLI, and Bower installed.
Here's how to build the dist version.
One Time Setup:
install node and npm (downloadable installer)
sudo npm install -g bower
sudo npm install -g grunt-cli
cd ~/repos/df-admin-app (or wherever your repo is)
npm install
bower install (creates sym link app/bower_components)
The app was built using Sass/Scss and compiled with Compass. This requires Ruby and Compass. Follow this guide to set it all up.
Then to rebuild dist folder:
grunt build
Or you can simply run a development mode using Docker:
docker-compose up
To build with Docker:
docker-compose run --rm web-dev bash -c "grunt build --force && chmod -R a+rwX ."
The final compiled CSS will be written to app/styles/styles.css
.
This pushes to master. Running composer update will pull down new version. The basic steps for doing a release with git-flow are as follows. Your local develop and master should be up to date before attempting this.
git checkout master
git pull origin master
git checkout develop
git pull origin develop
git flow release start 2.12.2
Bump the app version in app/scripts/app.js
.
// Set application version number
.constant('APP_VERSION', '2.12.2')
grunt build
git add --all
git commit -m "Release 2.12.2"
git flow release finish 2.12.2
git push origin develop
git checkout master
git push origin master
git push --tags
The app can be configured to manage your DreamFactory instance from another remote server. Simply open the app.js
file contained in app/scripts
directory and add your DreamFactory instance host name to the INSTANCE_BASE_URL
constant at the top. You can now optionally build the app and deploy the dist
directory. You must enable CORS in the DreamFactory instance you will be deploying the app to.
In app/styles/sass/partials
you can find the stylesheets for all the custom parts of the app as well as a few bootswatch templates in the themes
directory. All of these are added in a specific order in styles.scss
. To change to a different bootswatch theme simply change all occurrences of the theme name in styles.scss
. Dont forget to run grunt build
to compile the stylesheets and build the app.
The app was designed to have plugable modules. Every module contains it's own routes, events, and logic so as to remove one would not stop the app from working. These modules are stored under app/admin_components
. In order to faciliate speed a module was designed as a central repository for data that is used frequently in the app. Many other modules rely on this module for data to do their job but with a small bit of refactoring it can be removed to produce truly untethered modules.
The main application files are located in two directories. scripts
and views
located under the app
directory. The scripts
directory contains your app.js file and a sub directory called controllers
contains main.js
. Corresponding views for controllers defined in main.js
can be found in the aforementioned views
directory. The app.js
file contains a few constants. The ones of note are the INSTANCE_BASE_URL
, INSTANCE_API_PREFIX
, and APP_API_KEY
. The INSTANCE_BASE_URL
allows a host to be set which the application and it's modules will refer to for api calls. INSTANCE_API_PREFIX
can be changed to match the server setup. APP_API_KEY
is used in a config option defined below the constants that sets the api key for all calls made from the app. app.js
also defines standard routes for login, logout, registering. These routes have corresponding controllers defined in main.js
.
main.js
defines app specific controllers. The MainCtrl acts as a top level scope which other modules can query for app wide data. For example, our top level navigation and component navigation links are stored here in arrays which are passed to directives that render the links and control active link highlighting. Whenever a module is added/removed it's link will need to be handled here. But you shouldn't encounter this very often (or at all).
Authentication controllers provide attachment points for authentication/register events. They implement sparse logic in dealing with auth/register events produced by the user management module. This provides a decoupling between app specific logic for auth/register and the business logic of actually authenticating/registering a user. See main.js
comments for more info.
A data repository module called dfApplicationData
facilitates the loading and management of frequently used application data. It creates an object called dfApplicationObj
. It contains generic methods to access, modify, and delete data in the application and on the server. It also provides accessor methods to retrieve and save the actual dfApplicationObj. While not recommended to interact with this object directly it is sometimes a necessary evil. The module also contains init code to check whether it is necessary to build a new app object or to refresh the screen with local data as well as what apis to load.
The utility module provides services, factories, directives, and filters related to the operation of modules. Things like our icon service, navs, table filtering/pagination, etc are stored here. Basically, things that multiple modules may need access to and/or have no other place to go.
A module is defined in the usual AngularJS fashion. angular.module(MODULE_NAME, [DEPENDENCIES])
. Below that line we define a few constants and config for the module. Because modules are generally small SPA's we have included only one main route. A sub section of the dfApps
module is shown below to illustrate this point.
// Module definition
angular.module('dfApps', ['ngRoute', 'dfUtility', 'dfApplication', 'dfHelp', 'dfTable'])
// Path constants are defined to facilitate ease of reorganization
.constant('MOD_APPS_ROUTER_PATH', '/apps')
.constant('MOD_APPS_ASSET_PATH', 'admin_components/adf-apps/')
// A Route for the module is configured and a bit of access logic is included
.config(['$routeProvider', 'MOD_APPS_ROUTER_PATH', 'MOD_APPS_ASSET_PATH',
function ($routeProvider, MOD_APPS_ROUTER_PATH, MOD_APPS_ASSET_PATH) {
$routeProvider
.when(MOD_APPS_ROUTER_PATH, {
templateUrl: MOD_APPS_ASSET_PATH + 'views/main.html',
controller: 'AppsCtrl',
resolve: {
checkUser:['checkUserService', function (checkUserService) {
return checkUserService.checkUser();
}]
}
});
}])
.run([function () {
}])
// More module code
Every component module is designed in this way. Modules will usually have a controller where module navigation(sidebar links) will be stored along with a module title.
// Module config/routes/constants
.controller('AppsCtrl', ['$scope', function($scope) {
// Set Title in parent
$scope.$parent.title = 'Apps';
// Set module links
$scope.links = [
{
name: 'manage-apps',
label: 'Manage',
path: 'manage-apps'
},
{
name: 'create-app',
label: 'Create',
path: 'create-app'
},
{
name: 'import-app',
label: 'Import',
path: 'import-app'
},
{
name: 'app-groups',
label: 'Groups',
path: 'app-groups'
}
];
// Set empty section options
// additional logic if there are no apps present
$scope.emptySectionOptions = {
title: 'You have no Apps!',
text: 'Click the button below to get started building your first application. You can always create new applications by clicking the tab located in the section menu to the left.',
buttonText: 'Create An App!',
viewLink: $scope.links[1]
};
$scope.$on('$destroy', function (e) {
});
}])
Each module has a main.html
. In main.html
there wil be directives that pertain to module funcitonality. There is a sidebar navigation directive that takes the links
array and generates navigation. A few ng-if statements render the properly selected view. Here is the dfApps
module's main.html
file.
<div>
<div class="col-md-2 df-sidebar-nav">
<df-sidebar-nav></df-sidebar-nav>
</div>
<div class="col-md-10 df-section" df-fs-height >
<df-manage-apps data-ng-if="activeView.path === 'manage-apps'"></df-manage-apps>
<df-app-details data-ng-if="activeView.path === 'create-app'" data-new-app="true"></df-app-details>
<df-import-app data-ng-if="activeView.path === 'import-app'"></df-import-app>
</div>
</div>
If you are experienced with AngularJS you can see that most of if not all of the modules work is delegated to directives as opposed to using routes and controllers. While this doesn't allow for deep linking it does offer the convenience of dealing with data via context. Most modules will have manage and detail contexts which are modeled as directives and have templates stored locally in relation to the modules folder. The manage directive will pull data from the repository, create an object(referred to as a 'Managed Object') based on that data, and present it in a tabular fashion. There is usually behavior associated with the Managed object (for example, managed app objects allow you to launch a hosted app from it's Managed Object state). Upon selection the managed object will be passed to a detail directive which will create an object of that type for editing or creation. When that detail directive detects data the manage context is closed and the detail context is shown. This is usually a form for editing the currently selected object. The object can be saved, updated, or closed with changes discarded. Basic CRUD stuff. Once an operation has been completed the detail context can be closed and the manage context will reappear. We save deleting for the manage context where one or multiple objects can be selected and deleted.
All the directives(contexts) work in a similar fashion of having data passed to them and then creating an object that holds that data. The data passed to a directive will always be encapsulated in the 'record' property of the created object. Often we attach other UI specific properties to that object which will be found under the __dfUI
property. Below is an example of what an App object looks like in the detail context.
{
__dfUI: {
selected: false
},
record: {
// app data for editing
},
recordCopy: {
// copied app data for comparison
}
}
All form models are tied to the data stored on the record property. When we save/update the app a new App object is created to replace the old one. You are then free to close the detail view or continue editing. If you have made changes and attempt to close without saving then the comparison of the record and recordCopy will fail prompting you if you 'Are sure you want to close?'. That's how most of it works. Pretty simple. Pull data and create objects. Select object to edit. Show form to edit object. Compare on save and close.
Each one of the directives is organized in a similar fashion. See the stubbed out example below:
.directive(DIRECTIVE_NAME, [function() {
return {
restrict: 'E',
scope: {
thingData: '='
},
templateUrl: CONSTANT_PATH + 'views/TEMPLATE.html',
link: function (scope, elem, attrs) {
// LOCAL FUNCTIONS:
// things pertaining to this directive that won't be shared or stored on scope
// Object constructors usually
var Thing = function (thingData) {
var thingModel = {
thingName: 'My Awesome thing name'
};
thingData = thingData || thingModel;
return {
__dfUI: {
selected: false,
hasError: false
},
record: thingData,
recordCopy: angular.copy(thingData);
}
};
scope.theThing = null;
// PUBLIC API
// Scope functions that attach to our UI
// we do preliminary checking here
scope.saveThing = function () {
if (scope.theThing.__dfUI.hasError) {
alert('Thing has error');
return;
}
scope._saveThing()
};
// PRIVATE API
// functions stored on/off scope that provide
// targeted functionality
scope._myPrivFuncOne = function () {
// Do someting
};
scope._saveThingToServer = function () {
// Save thing to server. Return promise
};
// COMPLEX IMPLEMENTATION
// These scope functions generally are called from the public api
// Their names usually correspond with a preceding underscore
// We call private api functions targeted for specific tasks
// and build our...COMPLEX IMPLEMENTATION of the public function.
scope._saveThing = function () {
// private func to do someting
scope._myPrivFuncOne();
// save thing to server
scope._saveThingToServer(scope.theThing).then(
function (result) {
scope.theThing = new Thing(result.data)
},
function (reject) {
//report error
}
);
};
// WATCHERS
// place any watchers here
var watchThing = scope.$watch('thingData', function (newValue, oldValue) {
scope.theThing = new Thing(newValue);
};
// MESSAGES
// Handle messaging/events here
scope.$on('$destroy', function (e) {
watchThing();
};
}
}])