Sitecore Sidekick – App Tutorial / Log Viewer

Here in lies a tutorial for building your own Sidekick app.  In this example we’ll be building an app that exposes running and recently completed Sitecore jobs (like publishing, and index rebuilds).  I believe Sitecore 8.2 has something to do this out of the box, however those of us on older versions of Sitecore are at the mercy of the black box if we happen to accidentally close the publishing dialog.  It’s still going in the background and blocking further publishing operations, but is it stuck?  We don’t know!  Lets arm ourselves with knowledge!

jobviewer

You can grab the source code here in a stand alone project HERE
Note that this requires the Sitecore nuget feed.

Angularjs parts

In your visual studio solution create a folder for resources.  In this folder put all your angular code and very important edit the file properties on each js file to compile it as an embedded resource instead of the default “content”. Sidekick has a special way of serving these content that aggregates all resources into a single dll file. This minimizes installation and file system mess. As well as making the final product of any Sidekick app be simply a DLL and a .config file. No mess, no fuss!

Step one:

Build an angular factory.  To act as a hub to access the server side information.

(function () {
    'use strict';

    angular
        .module('app')
        .factory('JVFactory', JVFactory);

    JVFactory.$inject = ['$http'];

    function JVFactory($http) {
        var service = {

            getJobs: function (running) {
                var data = { "running": running};
                return $http.post("/scs/jvgetjobs.json", data);
            }
        };

        return service;

        function getData() { }
    }
})();

Step two:

Build an angular directive. This will be the root of our app from a DOM perspective.

(function () {
    'use strict';

    angular
        .module('app')
        .directive('jvdirective', jvdirective);

    function jvdirective() {

        var directive = {
            templateUrl: "/scs/jvmaster.scs",
            restrict: 'EA'
        };
        return directive;
    }

})();

Step three:

Build an angular js html template. This will be the actual markup for your app. Note that instead of the traditional html templates. In Sidekick i would recommend you use .scs files to prevent rare collisions in Sitecore’s routing of html files

<div ng-controller="jvcontroller as vm" class="jvmain">
	<div class="btn" ng-if="vm.runningJobs" ng-click="vm.runningJobs = false">Show Completed Jobs</div>
	<div class="btn" ng-if="!vm.runningJobs" ng-click="vm.runningJobs = true">Show Running Jobs</div>
	<div ng-repeat="job in vm.jobs">
		<div>
			<h4>
				<a href="#" ng-click="vm.toggleDetails(job.Name+job.Category+job.TimeStarted)">{{job.Name}}</a>
			</h4>
			<p>
				<strong>Processed:</strong> {{job.ProcessedItemsCount}}
			</p>
			<div ng-if="vm.selectedJob === job.Name+job.Category+job.TimeStarted">
				<p>
					<strong>Category:</strong> {{job.Category}}
				</p>
				<p>
					<strong>Started:</strong> {{job.TimeStarted}}
				</p>
				<p>
					<strong>Job State:</strong> {{job.State}}
				</p>
				<p style="border-bottom:2px solid black">
					<strong>Job Messages</strong>
				</p>
				<pre>{{job.Messages}}</pre>
			</div>
		</div>
	</div>
</div>

Step four:

Build an angular controller. This will control the angular scope that we’re going to use to control the data on the front end.

(function () {
    'use strict';

    angular
        .module('app')
        .controller('jvcontroller', jvcontroller);

    jvcontroller.$inject = ['JVFactory', '$interval'];

    function jvcontroller(JVFactory, $interval) {
        /* jshint validthis:true */
        var vm = this;
        vm.runningJobs = true;
        vm.getJobs = function () {
            JVFactory.getJobs(vm.runningJobs).then(function (response) {
                vm.jobs = response.data;
            }, function (response) {
                vm.error = response.data;
            });
        }
        vm.toggleDetails = function (jobkey) {
            if (vm.selectedJob === jobkey)
                vm.selectedJob = false;
            else
                vm.selectedJob = jobkey;
        }
        $interval(function () {
            vm.getJobs();
        }, 500);
        vm.getJobs();
    }
})();

Misc Resources

In the resources folder you created for the angular code put all your file based assets and very important edit the file properties on each file to compile it as an embedded resource instead of the default “content”

Step one:

Css. We need to make our app look reasonable. It’s important to note that you need to make your css as app specific as possible, since all Sidekick apps run in the same angular app if you set up styles to say the div tag as a whole, it will effect other apps.

.jvmain div > div {
    border: 2px solid black;
	border-radius: 5px;
    margin-bottom: 5px;
    padding-left: 5px;
    background-color: #eee;
    display: inline-block;
    width: 96%;
    margin: 10px;
    word-wrap: break-word;
}
.jvmain div > div > div {
    margin-right: 5px;
    border: 2px solid blue;
    background-color: #ddd;
    width: 96%;
}
.jvmain strong {
	font-weight: 700;
	font-size: 14px;
}
.jvmain h4 {
	font-size: 16px;
	font-weight: 700;
}

Step Two:

Logo. For the Sidekick tile.

jvgearwheels

ASP.net parts

Step one:

Http Handler.  In Sidekick we have a special Http handler that’s wired up automatically through the Sidekick’s core.  It’s an abstract class called ScsHttpHandler.  We need to implement one of those. You’ll notice that this is where a lot of the magic bindings happen, for instance this is where you give the namespace of your resources folder that all our previous assets were created into. Note that there’s a method here GetPostData which gets all post data sent by angular and builds a dynamic C# object for you to use.

	public class JobViewerHandler : ScsHttpHandler
	{
		public JobViewerHandler(string roles, string isAdmin, string users)
			: base(roles, isAdmin, users)
		{
		}

		public override string Directive { get; set; } = "jvdirective";
		public override NameValueCollection DirectiveAttributes { get; set; }
		public override string ResourcesPath { get; set; } = "ScsJobViewer.Resources";
		public override string Icon => "/scs/jvgearwheels.png";
		public override string Name => "Job Viewer";
		public override string CssStyle => "width:600px";
		public override void ProcessRequest(HttpContextBase context)
		{
			string file = this.GetFile(context);

			if (file == "jvgetjobs.json")
			{
				this.ReturnJson(context, this.GetJobs(context));
			}
			else
			{
				this.ProcessResourceRequest(context);
			}
		}
		private object GetJobs(HttpContextBase context)
		{
			var data = GetPostData(context);
			var model = JobManager.GetJobs().Where(x => data.running ? !x.IsDone : x.IsDone).OrderBy(x => x.QueueTime);
			return model.Select(x => new JobModel(x));
		}
	}

Step two:

In order to run the above code we need a model

	public class JobModel
	{
		private readonly Job _job;
		public JobModel(Job job)
		{
			this._job = job;
		}
		public string Name => this._job.Name;
		public string Category => this._job.Category;
		public string State => this._job.Status.State.ToString();
		public string Messages
		{
			get
			{
				StringBuilder sb = new StringBuilder();
				sb.AppendLine("Job Details for " + this.Name);

				if (this._job.Options.ContextUser != null)
				{
					sb.AppendLine("Context User: " + this._job.Options.ContextUser.Name);
				}

				sb.AppendLine("Priority: " + this._job.Options.Priority);
				sb.AppendLine("Messages:");

				foreach (string s in this._job.Status.Messages)
				{
					sb.AppendLine(s);
				}

				return sb.ToString();
			}
		}
		public bool IsDone => this._job.IsDone;
		public long ProcessedItemsCount => this._job.Status.Processed;
		public long TotalItemsCount => this._job.Status.Total;
		public bool HasTotalItems => this.TotalItemsCount > 0;
		public string TimeStarted => this._job.QueueTime.ToLocalTime().ToString("G");
	}

The configuration

This step is simple, we just need to register our app with sidekick.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <scsRegister>
        <processor type="ScsJobViewer.JobViewerHandler, ScsJobViewer" >
          <!-- leave blank for any role -->
          <param name="roles"></param>
          <!-- set to "true" to only allow admins-->
          <param name="isAdmin"></param>
          <!-- leave blank for any users -->
          <param name="users"></param>
        </processor>
      </scsRegister>
    </pipelines>
  </sitecore>
</configuration>