Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer. You can expect quick tips, links to interesting tutorials, opinions and packages. Want to learn the cool stuff? Then sign up now!

Building a dashboard using Laravel, Vue.js and Pusher

At Spatie we have a tv screen against the wall that displays a dashboard. This dashboard displays the tasks our team should be working on, important events in the near future, which music is playing at our office, and so on. Here’s what it looks like:

screenshot

We’ve opensourced our dashboard, so you can view the entire source code on GitHub. It is built with Laravel 5.3 and Vue. In this post I’d like to explain why and how we made it.

You’ll need to be familiar with both Laravel and Vue to get the most out of this post. If you need to brush up your Vue knowledge, I can highly recommend Laracast’s lessons on Vue.

History

We’ve had a dashboard at Spatie for quite some time now. Before our current Laravel-based one we used Dashing, a framework to quickly build dashboards. The framework was created by the folks at Shopify and uses Ruby under the hood.

The time I first built our dashboard, a few years ago, we were at a crossroads at Spatie. There wasn’t much momentum in the PHP world and we were toying with the idea of switching over to Ruby. The idea was that by playing with Dashing we would get some experience with the language. Then Composer and Laravel happened and we decided to stick with PHP (and given the current state of PHP ecosystem we don’t regret that choice at all).

Now that support for Dashing has officially stopped, I thought it was a good time to completely rebuild the dashboard using Laravel and Vue.

High level overview

Let’s take a closer look at what the dashboard is displaying. The configured dashboard from the above screenshot has the following tiles:

  • A team calendar that pulls in it’s events of a Google Calendar.
  • The music that is currently playing in our office, fetched from Last.fm‘s API.
  • A clock with the current date.
  • The todo’s of each member of our team. It’s managed through a few markdown files in a private repo on GitHub.
  • Stars and number of package downloads are pulled in via the Packagist API.
  • A rain forecast (for the bikers amongst us) via buienradar.nl.
  • Internet up/down status.

After the browser displays the dashboard for the first time we’ll never refresh the page again. WebSockets and Vue are being leveraged to update the tiles. Doing it this way will avoid having to refresh the page and in turn avoid flashing screens.

Each tile is it’s own Vue component. Laravel’s default scheduler is used to periodically fetch some data from the API’s of Google Calendar, Last.fm, etc… When Laravel receives a response from any of those services a broadcast event is fired to Pusher. This powerful service leverages websockets to carry over server events to clients in realtime. On the client side we’ll use Laravel Echo. That JavaScript library makes handling those Pusher events very easy. Still on the client side each Vue component will listen for incoming events to update the displayed tiles.

The grid

Before diving into the Laravel and Vue code I’d like to explain how the grid system works. The grid system allows you to easily specify where a tile should be positioned on the dashboard and how big it should be.

This is the content of the actual blade view that renders the dashboard page.

@extends('layouts/master')

@section('content')

    @javascript(compact('pusherKey'))

    <google-calendar grid="a1:a2"></google-calendar>

    <last-fm grid="b1:c1"></last-fm>

    <current-time grid="d1" dateformat="ddd DD/MM"></current-time>

    <packagist-statistics grid="b2"></packagist-statistics>

    <rain-forecast grid="c2"></rain-forecast>

    <internet-connection grid="d2"></internet-connection>

    <github-file file-name="freek" grid="a3"></github-file>

    <github-file file-name="rogier" grid="b3"></github-file>

    <github-file file-name="seb" grid="c3"></github-file>

    <github-file file-name="willem" grid="d3"></github-file>

@endsection

Grid columns are named with a letter and rows are named with a number, like a spreadsheet. The size and positioning of a tile is determined in a grid property per component that accepts a column name and a row number. a1 will render the component on the first row in the first column. If you look at the first github-file component you see a3, so as you see in the screenshot of the dashboard, this component will get displayed in the first column on the third row.

You can also pass a range to the grid attribute. The last-fm component is a good example. Setting grid to b1:c1 causes it to be displayed in the second an third columns of the first row.

Our dashboard uses three rows and four columns. Want to change the size of your dashboard? No problem: those numbers can be adjusted in _grid.scss:

$grid: (
    cell-spacing: 1vw,
    cell-padding: 1vw,
    cols : 4,
    rows : 3,
);

Most modern tv’s use a 16:9 ratio, but we’ve gone the extra mile to make the layout fully responsive so it still works on tv’s and monitors with a different ratio.

The looks of the dashboard and the grid system were designed by my colleague Willem. He did an awesome job creating custom dashboard layouts as easy as possible. If you want to know more about how the grid system works internally, you should read his guest post.

The internet connection component

Let’s take a deeper look at a component to grasp the general flow. A simple one is the internet-connection tile which notifies us when the internet connection is down. It works by listening to an event, called Heartbeat, that is sent out every minute by the server. When it doesn’t get an event within a couple of minutes it’ll determine that our internet connection is down (although it could also mean that the server where the dashboard is running on is having problems).

Server side

In the Laravel app you’ll see a directory app/Events. All events are sent from the server to the client reside there. In that directory you’ll see a file named DashboardEvent which is used to transfer data from the server to the client through events.

namespace App\Events;

use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

abstract class DashboardEvent implements ShouldBroadcast
{
    /**
     * Get the channels the event should broadcast on.
     */
    public function broadcastOn()
    {
        return new PrivateChannel('dashboard');
    }
}

That ShouldBroadcast interface is provided by Laravel. All events will get broadcasted on the private channel named dashboard. The client will listen to all events on that channel. Using the PrivateChannel class will ensure that all data will be sent in a secure way so nobody can listen in. More on that later.

Let’s take a look at the app/Components directory. Almost all logic that the server needs to do to fetch data for the dashboard has a home here. If you open up that directory you’ll see that each component has it’s own subdirectory. In each subdirectory you’ll find an Artisan command that can be scheduled. In our example the App\Components\InternetConnectionStatus class contains this code:

namespace App\Components\InternetConnectionStatus;

use App\Events\InternetConnectionStatus\Heartbeat;
use Illuminate\Console\Command;

class SendHeartbeat extends Command
{
    /**
     * The console command name.
     *
     * @var string
     */
    protected $signature = 'dashboard:heartbeat';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Send a heartbeat to help the client verify if it is connected to the internet.';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        event(new Heartbeat());
    }
}

The only thing that this code does is send out a HeartBeat-event. This command is scheduled to run every minute in the Console kernel.

Client side

All JavaScript code used by the dashboard lives in the resources/assets/js directory. In resources/assets/js/app.js you see that the main Vue instance is being initialized on the body element:

new Vue({

    el: 'body',

    components: {
        CurrentTime,
        GithubFile,
        GoogleCalendar,
        InternetConnection,
        LastFm,
        PackagistStatistics,
        RainForecast,
    },

});

The components themselves live in the resources/assets/js/components directory. This is the code of the internet-connection.js inside that directory:

import Grid from './grid';
import moment from 'moment';
import Echo from '../mixins/echo';

export default {

    template: `
        <grid :position="grid">
            <section :class="online? 'up': 'down' | modify-class 'internet-connection' ">
                <div class="internet-connection__icon">
                </div>
            </section>
        </grid>
    `,

    components: {
        Grid,
    },

    mixins: [Echo],

    props: ['grid'],

    data() {
        return {
            online: true,
            lastHeartBeatReceivedAt: moment(),
        };
    },

    created() {
        setInterval(this.determineConnectionStatus, 1000);
    },

    methods: {
        determineConnectionStatus() {
            const lastHeartBeatReceivedSecondsAgo = moment().diff(this.lastHeartBeatReceivedAt, 'seconds');

            this.online = lastHeartBeatReceivedSecondsAgo < 125;
        },

        getEventHandlers() {
            return {
                'InternetConnectionStatus.Heartbeat': () => {
                    this.lastHeartBeatReceivedAt = moment();
                },
            };
        },
    },
};

There’s a whole lot going on in that code. Let’s break it down. The template key contains the html code that actually gets rendered. The css class that gets used depends the value of the state of the online variable on the Vue instance. (Again, if you’re having trouble following this, check the series on Vue on Laracasts). In the created method, which is fired as soon as the Vue component is created, we’ll make sure that a method on the Vue instance called determineConnectionStatus is fired every second. That function is responsible for determining the value of online. If the last received heartbeat is less than 125 seconds ago, online will be true, otherwise it will be false.

Let’s review how we can listen for events. In the code above you’ll see a method called getEventHandlers. It expects an object of which the property names are the the events names. The event name is the fully qualified class name of the event that get sent out by the server (App\Events\InternetConnectionStatus) but without App\Events and with the \ replaced by a .. So in our example that would become InternetConnectionStatus.Heartbeat. The value of the of a property on that object should be a function that will get executed whenever that event comes in from the server. So in our case whenever the InternetConnectionStatus.Heartbeat.HeartBeat event comes in we’re going to set the state of lastHeartBeatReceivedAt to the current time. So if this event comes in the determineConnectionStatus function will determine that we’re online for the next 125 seconds.

Did you notice that the component uses a Echo mixin? A mixin can be compared to a trait in the PHP-world. A mixin contains some functions. Every component that uses the mixin will get those functions. So, like a trait, a mixin is a way to bundle reusable code.

The Echo mixin is responsible for adding the power of Laravel Echo to the component. Laravel Echo is a JavaScript library that makes it easy to handle websockets. Under it’s hood all authentication and communication with Pusher is handled. Echo is being setup in resources/assets/js/app.js.

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: dashboard.pusherKey,
});

Laravel Echo can handle multiple broadcasters, we’re going to use Pusher here. That key is a public value that’s needed to communicate with Pusher.

Let’s go back take a look at the code of the Echo mixin.

import { forIn } from 'lodash';

export default {
    created() {

        forIn(this.getEventHandlers(), (handler, eventName) => {

            let fullyQualifiedEventName = `.App.Events.${eventName}`;

            Echo.private('dashboard')
                .listen(fullyQualifiedEventName, (eventName) => {
                    handler(eventName);
                });
        });
    },
};

Whenever a component that uses the mixin is created the created function will be executed. It will process the output of getEventHandlers function from the component itself. First, we’ll build up the fully qualified event name. Then we’ll let Echo listen for events with that name on the private dashboard channel. Whenever an event with the right name comes in we’re going to execute the handler.

The Package statistics component

Let’s take a look at another component. In the screenshot of the dashboard you can see that there are some statistics displayed regarding how many times our packages get downloaded.

The FetchTotals class, located in app/Components/Packagist/FetchTotals.php is responsible for fetching the package statistics via the Packagist API files on GitHub, and transforming it an array. After that it’ll fire off an event to inform the Vue side of things that new data is available.

namespace App\Components\Packagist;

use App\Events\Packagist\TotalsFetched;
use GuzzleHttp\Client;
use Illuminate\Console\Command;
use Spatie\Packagist\Packagist;

class FetchTotals extends Command
{
    /**
     * The console command name.
     *
     * @var string
     */
    protected $signature = 'dashboard:packagist';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Fetch the total amount of downloads of packages for a vendor.';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $client = new Client();

        $packagist = new Packagist($client);

        $totals = collect($packagist->getPackagesByVendor('spatie')['packageNames'])
                ->map(function ($packageName) use ($packagist) {
                    return $packagist->findPackageByName($packageName)['package'];
                })
                ->pipe(function ($packageProperties) {
                    return [
                        'stars' => $packageProperties->sum('favers'),
                        'daily' => $packageProperties->sum('downloads.daily'),
                        'monthly' => $packageProperties->sum('downloads.monthly'),
                        'total' => $packageProperties->sum('downloads.total'),
                    ];
                });

        event(new TotalsFetched($totals));
    }
}

Most of this code should be self-explanatory. It’s also scheduled to run periodically. Let’s take a look at the TotalsFetched event that’s being sent out:

<?php

namespace App\Events\Packagist;

use App\Events\DashboardEvent;

class TotalsFetched extends DashboardEvent
{
    public $daily;

    public $monthly;

    public $total;

    public $stars;

    public function __construct($totals)
    {
        foreach ($totals as $sumName => $total) {
            $this->$sumName = $total;
        }
    }
}

When broadcasting events in Laravel, all public properties of an event are being broadcasted as well. So using this code the Vue component can easily get to the values of $daily, $monthly, $total and $stars.

Here’s the Vue Component that renders the tile on the dashboard:

import Grid from './grid';
import Echo from '../mixins/echo';
import SaveState from '../mixins/save-state';

export default {

    template: `
        <grid :position="grid" modifiers="padded overflow">
            <section class="packagist-statistics">
                <h1>Package Downloads</h1>
                    <ul>
                    <li class="packagist-statistic">
                        <span class="packagist-statistics__stars"></span>
                        <span class="packagist-statistics__count">{{ stars | format-number }}</span>
                    </li>
                    <li class="packagist-statistic">
                        <h2 class="packagist-statistics__period">Today</h2>
                        <span class="packagist-statistics__count">{{ daily | format-number }}</span>
                    </li>
                    <li class="packagist-statistic">
                        <h2 class="packagist-statistics__period">This month</h2>
                        <span class="packagist-statistics__count">{{ monthly | format-number }}</span>
                    </li>
                    <li class="packagist-statistic -total">
                        <h2 class="packagist-statistics__period">Total Downloads</h2>
                        <span class="packagist-statistics__count">{{ total | format-number }}</span>
                    </li>
                </ul>
            </section>
        </grid>
    `,

    components: {
        Grid,
    },

    mixins: [Echo, SaveState],

    props: ['grid'],

    data() {
        return {
            stars: 0,
            daily: 0,
            monthly: 0,
            total: 0,
        };
    },

    methods: {
        getEventHandlers() {
            return {
                'Packagist.TotalsFetched': response => {
                    this.stars = response.stars;
                    this.daily = response.daily;
                    this.monthly = response.monthly;
                    this.total = response.total;
                },
            };
        },

        getSavedStateId() {
            return 'packagist-statistics';
        },
    },
};

Notice that in the getEventHandlers function we’ll update the state variables stars, daily, monthly, total to the values that we get from the TotalsFetched-event.

Security

Because there is some semi-sensitive info being displayed (the tasks of our team members) we’ve added some security to the dashboard. That’s why you can’t just visit https://dashboard.spatie.be.

The url itself is protected by a basic auth filter on the routes. The filter is native to laravel package. Relying on basic auth can be a bit insecure. So if you are going to fork our dashboard be sure to pick a long password and do some rate limiting server side to prevent brute force attacks.

The data sent through the websockets is secured as well. In the Echo mixin you might have noticed that a private method is called. This will ensure that, under the hood, a private Pusher channel is used, so nobody can listen in to what is sent via the websockets.

Displaying the dashboard on a TV

Behind our tv there is a Raspberry Pi 2 that displays the dashboard. It is powered by a USB port in the tv and it has a small Wifi dongle to connect to the internet, so cables aren’t necessary at all.

IMG_0938 (1)

The Pi used the default Raspian OS. When it is powered on it’ll automatically launch Chromium 50 and display the contents of https://dashboard.spatie.be.

IMG_0939 (1)

Reloading the dashboard

The communication between the server and client is one-way. The client will receive data solely through the events sent out by the server. The client will never make a request for data itself.

When our last team member leaves the office he will switch off the wall mounted TV. This will cause the Pi to be powered down as well. The next time when the tv is powered back on the dashboard will be empty, waiting for events sent out by the server. We don’t want to stare at an empty dashboard the first hour after the tv is powered on, let’s fix that.

Every Vue component preserves it’s own state in data. Wouldn’t it be great to save that data whenever it is changed? Then it could be reloaded whenever the dashboard gets powered on.

The SaveState-mixin, which is used on almost every component, does exactly that.

export default {
    watch: {
        '$data': {
            handler() {
                this.saveState();
            },
            deep: true,
        },
    },

    created() {
        this.loadState();
    },

    methods: {
        loadState() {
            let savedState = this.getSavedState();

            if (!savedState) {
                return;
            }

            this.$data = savedState;
        },
        saveState() {
            localStorage.setItem(this.getSavedStateId(), JSON.stringify(this.$data));
        },

        getSavedState() {
            let savedState = localStorage.getItem(this.getSavedStateId());

            if (savedState) {
                savedState = JSON.parse(savedState);
            }

            return savedState;
        },
    },
};

The mixin watches the data of the component it’s applied to. Whenever data changes, saveState gets called, which serializes the data and writes it to local storage. Afterwards, when the component is created, the mixin will restore it’s state. This means that when we power on our tv the saved data will immediately be displayed.

Packages used

The dashboard is fetching data from various sources: Google Calendar, Packagist, Lastfm,… Here’s the list of packages used to pull in the data:

Alternatives

We mainly built our own custom Dashboard to toy around with Vue and also just because it’s fun.

There are many alternatives available to display dashboards:

Choose a solution that feels good to you.

Closing notes

I hope that you’ve enjoyed this little tour of the code behind our dashboard. I don’t have the intention of creating full documentation for the dashboard and make it monkey-proof as that would require just too much time. On the other hand I do think that if you have some experience with Laravel and Vue it’s not that hard to make your own dashboard with our code.

We’ll be sure to keep our Dashboard updated so it uses the latest versions of frameworks and packages. When Laravel 5.3 is released the code will get updated to use the latest features of the framework, most notably Laravel Echo. A new major version of Vue is also somewhere around the corner.

Again the entire source code is available on GitHub. If you have any questions on the dashboard, feel free to open up an issue on GitHub.

IMG_0937

EDIT on 2016-08-29: the dashboard was updated in to make use of Laravel 5.3 and Laravel Echo. This blog post was updated too to reflect those changes.

Freek Van der Herten is a partner and developer at Spatie, an Antwerp based company that specializes in creating web apps with Laravel. After hours he writes about modern PHP and Laravel on this blog. When not coding he’s probably rehearsing with his kraut rock band.
  • Pingback: Building a Status Board using Laravel and Vue - Laravel News()

  • Tihomir Opacic

    Great article, thanks for the details Freek!

  • Pingback: Building a Status Board using Laravel and Vue – Rifix Blog()

  • Prosper

    Awesome article!

  • Pretty cool, I’ve been toying with Vue / Laravel and this just gives me more things to build with it.

  • Pingback: Freek Van der Herten: Building a dashboard using Laravel and Vue – SourceCode()

  • Pingback: Building a dashboard using Laravel, Vue.js and Pusher | myWiki()

  • Sacha Telgenhof

    Super article, Freek! I have been playing around with Dashing some time ago, and wanted to develop my own (Laravel) as Dashing is not actively developed any longer. Your article and code is for sure a nice welcome!

    BTW, do you have some instructions how you’ve setup Chromium? Like how to have it autostart, etc..? Thanks!

  • Nice one! Looks like it was a fun project too.

  • Tim

    Have you considered using cec to turn the tv on/off on a schedule?

    • We’re not always at the office at regular times. We keep it simple by just using the remote control to turn the tv on and off.

  • Nyan Lynn Htut

    Great article, thanks for sharing Freek

  • Pingback: How TNW creates a smart office experience with Raspberry Pi()

  • Ryan Dobbs

    Nice. Thanks for sharing!

  • Pingback: June 2016 Review | Andre Madarang()

  • jimmyrolando

    Great post!, Thank you for sharing, I have a little repo on github, where i’m trying to integrate laravel + vuejs + vuex https://github.com/jimmyrolando/laravel-vuejs-vuex I would appreciate your feedback

  • Pingback: Интересное из мира PHP #1 | Straven-Loft. Лофт заметок, мыслей, идей()

  • Pingback: webMASTAH.weekly.018 – Dziewczyno! Nie nadajesz się do programowania! – webMASTAH()

  • od3n

    How to check whether my cron & Pusher is running correctly?

  • Benjamin Fox

    Hey Freek, Thanks for this great article.. Really appreciate it 🙂

    I’ve been out of coding for about 10 years (working as a IT Project Manager now) but I have a need to do some code again (personal project).

    I’ve spent the past week, getting up to speed on current technologies, angular, node, php, vue and trying to determine the best stack to utiilise for my project.

    I was recommended to utilise vue and laravel by a couple of mates..

    Your tutorial explanation above is super cool… Firstly, because I want to take the idea to my current job….and secondly and obviously my own project.

    I feel like such a raw beginner again ( my previous coding was all Enterprise Java… doing struts)… the world has changed a LOT in the past 10 years!!!

    Thanks again.
    Benjamin

    • Hi Benjamin,

      I’m glad you liked the article.

      Here are some great resources to get you started with PHP / Laravel / Vue
      http://www.phptherightway.com/
      https://laracasts.com/ (and for vue, this series: https://laracasts.com/series/learning-vue-step-by-step)
      – not out yet, but it will be great, trust me: https://laravelupandrunning.com/

      Have fun!

      • Benjamin Fox

        Heya Freek,

        Thanks for your reply! Really appreciate it mate! 🙂

        I’d found laracasts (what an awesome website that is)… Started going through the vue series videos as well.

        What i’m trying to work out is getting my requirements / scope into reality.

        I look at the bootstrap websites and really like some of the styles that have been incorporated ( i see some great admin / dashboards on themeforest) that look good.

        It’s working how to incorporate that UI into Laravel / Vue…. plus understanding the backend as well!

        Soooooooo much to learn…. I suppose it’s starting simple…. and going from there.

        Thanks for the tip to Laravel… From my reading it definitely seems the php variant of choice.

        Thanks for helping out an Aussie mate!

        Take care.
        Benjamin

  • Pingback: Our dashboard has been updated to make use of Laravel Echo - murze.be()

  • ibr_oss

    Great article, thanks!

  • Caio Cezar Viana

    Awesome, thanks for sharing the experience.

  • Jason Miller

    I just grabbed the code. I am looking at setting it up over this weekend and playing with different features. Love what you have done thought. I’ve now browsed your repo and want to say thanks – Ill send a wicked cool postcard if i can get my hands on one soon!! Thanks!!!

  • Nabil Ftd

    Awesome tutorial.

  • jplhomer

    This is amazing. The source code is helping me learn so much about Laravel event broadcasting. Great work!

  • Bobby Borisov

    Hi, i am trying to follow the tutorial and i started with fresh laravel install .. public channels work for laravel echo but i cannot make private ones to work. it keeps telling me that broadcasting/auth route is not found. And when i do php artisan route:list route is not there. I got your branch and i can see the broadcasting route. Any idea how to add it? Thanks in advance!

  • Manas N

    This is beautiful. I checked the github repo and the code written there is very fluent.