The birth of Henry's Dashboard

Dashboard

https://api.henry.wang

And some frontend practices.

Why

In light of my ever growing list of self-hosted services, where LDAP isn’t applicable to all of them, there is a need for an all-in-one dashboard-like page. This also serves as the homepage of my API instance

To make it a little bit more interesting, I decided it should have Chart.js to display corresponding statistics of that service. Also instead of traditional html + css, I decided to learn something new.

What I’ve Learned

Sassy CSS

Sassy CSS (SCSS) is a CSS preprocessor designed to improve CSS coding experience. Before then I was mixing SCSS with SASS (Syntactically Awesome Style Sheets), and it turns out they are not identical.

Nesting and Variable

/* CSS */

.logo {
  margin: 0 1em -2em 0;
}

.logo span {
  color: #f2e4d6;
}

.headline {
  margin: 0 1em -2em 0;
}

.headline .content {
  background-color: #f2e4d6;
}
// SCSS

$margin-medium: 0 1em -2em 0;
$color1: #f2e4d6;

.logo {
  margin: $margin-medium;
  span {
    color: $color1;
  }
}

.headline {
  margin: $margin-medium;
  .content {
    background-color: $color1;
  }
}
// SASS

$margin-medium: 0 1em -2em 0
$color1: #f2e4d6

.logo
  margin: $margin-medium
  span
    color: $color1

.headline
  margin: $margin-medium
  .content
    background-color: $color1
  • CSS is behaving CSSly
  • SASS is the most concise one
  • SCSS is SASS with CSS syntax, which is also the reason why I chose it (who doesn’t like closing semicolons!).

Inheritance, Function and Mixin

// SCSS

@mixin border-radius-mixin($radius) {
  -webkit-border-radius: $radius;
  -moz-border-radius: $radius;
  -ms-border-radius: $radius;
  border-radius: $radius;
}

@function border-radius-func($radiusA, $radiusB) {
    @if ($radiusA > $radiusB){
        @return $radiusA - $radiusB;
    }
    @return $radiusB;
}

.roundBox {
  @include border-radius-mixin( 
    border-radius-func(10px, 8px)
  ); 
}

.bigDiv{
  @extend .roundBox;
  font-size: 3em;
}

Import

/* CSS */

h2 {
  font-weight: 900;
}

p {
  font-weight: 700;
}

span {
  font-weight: 500;
}
// _font-weight.scss

$weight: 900;

$types: h2 p span;

@each $type in $types {
  #{$type} {
    font-weight: $weight;
  }
  $weight: $weight - 200;
}
// --- in_some_other.scss ---

@import "_font-weight";

There are also many other features introduced in the official guide that I don’t use currently. In short, SCSS drastically improves the reusability and that’s absolutely crucial.

Jade Templating

Jade is a html template engine, which allows you to produce html without opening/closing all the annoying tags. Indentation is crucial in Jade.

// layout.jade

body
  .wrapper
    .content
      .content-wrapper
        .logo
          img(src="images/logo.jpg", width="100%")
          h1
            | Henry's Dashboard
        .grid-container
          block content <-- this is allows nesting/layout/partial view, whatever you call it.
    .footer-menu
      .menu.github#github Github
      .menu.email#email Email
      .menu.linkedin#linkedin LinkedIn
// index.jade

extends layout

block content <-- this will be included into the content block in layout.jade
 .gc-1-2
    .grid.service#rss
      .headline.content.atd
        | RSS
      .content
        | Daily news driver
      .footline Credit: Tiny Tiny RSS
    .grid
      .upper-headline Feed Stats
      .chart(style="position:relative; height:140px;")
        canvas#rssChart

After learning Jade, it looks like I could’ve got used to SASS too.

Build Automation using Gulp

Another objective of building this page, is to explore more npm packages, in this case, it was the amazing gulp.

Using gulp I achieved:

  1. SCSS was compiled into browser-readable CSS(gulp-sass).
  2. Js files were translated into browser-readable versions(via gulp-babel) with all dependencies(via gulp-browserify) and combined into a single js file(via gulp-concat).
  3. Both CSS and JS are all minified to improve loading speed, but gulp-sourcemaps still allows them to be reverted back during console debugging.
  4. Reduced amount of configurations needed (previously I was using babel, uglify and browserify separately).

Gulp 3

// in gulpfile.js

const gulp = require('gulp');
const sass = require('gulp-sass');
const sourcemaps = require('gulp-sourcemaps');
const concat = require('gulp-concat');
const uglify = require('gulp-uglify');
const browserify = require('gulp-browserify');
const babel = require('gulp-babel');
const del = require('del');

const paths = {
  scss: ['./scripts/*.scss'],
  js: ['./scripts/*.js'],
};

gulp.task('clean', () => del(['./public/css'], ['./public/js']));

gulp.task('scss', ['clean'], () =>
  gulp
    .src(paths.scss)
    .pipe(sourcemaps.init())
    .pipe(sass({
        outputStyle: 'compressed',
      }).on('error', sass.logError),)
    .pipe(sourcemaps.write())
    .pipe(gulp.dest('./public/css')),);

gulp.task('js', ['clean'], () =>
  gulp
    .src(paths.js)
    .pipe(sourcemaps.init())
    .pipe(babel({
        presets: ['env'],
      }),)
    .pipe(browserify({
        insertGlobals: true,
      }),)
    .pipe(uglify())
    .pipe(concat('index.js'))
    .pipe(sourcemaps.write())
    .pipe(gulp.dest('./public/js')),);

gulp.task('watch', () => {
  gulp.watch(paths.scss, ['scss']);
  gulp.watch(paths.js, ['js']);
});

gulp.task('default', ['scss', 'js']);
// in package.json

"scripts": {
  "start": "npm run build && node ./bin/www",
  "build": "./node_modules/.bin/gulp"
}

Gulp 4

On 18 May I migrated to Gulp 4 which required some modifications to the config file.

// in gulpfile.js
const gulp = require('gulp');
const sass = require('gulp-sass');
const sourcemaps = require('gulp-sourcemaps');
const concat = require('gulp-concat');
const uglify = require('gulp-uglify');
const browserify = require('gulp-browserify');
const babel = require('gulp-babel');
const del = require('del');

const paths = {
  scss: ['./scripts/*.scss'],
  js: ['./scripts/*.js'],
};

gulp.task('clean', () => del(['./public/css'], ['./public/js']));

gulp.task('scss', () =>
  gulp
    .src(paths.scss)
    .pipe(sass({
      outputStyle: 'compressed',
    }).on('error', sass.logError))
    .pipe(sourcemaps.write())
    .pipe(gulp.dest('./public/css')));

gulp.task('js', () =>
  gulp
    .src(paths.js)
    .pipe(babel({
      presets: ['env'],
      plugins: [
        [
          'transform-runtime',
          {
            polyfill: false,
            regenerator: true,
          },
        ],
      ],
    }))
    .pipe(browserify({
      insertGlobals: true,
    }))
    .pipe(uglify())
    .pipe(concat('index.js'))
    .pipe(sourcemaps.write())
    .pipe(gulp.dest('./public/js')));

gulp.task('watch', () => {
  gulp.watch(paths.scss, gulp.series('scss'));
  gulp.watch(paths.js, gulp.series('js'));
});

gulp.task(
  'default',
  gulp.series('clean', gulp.parallel('scss', 'js'), 'watch'),
);

gulp.task(
  'deploy',
  gulp.series('clean', gulp.parallel('scss', 'js')),
);

Conclusion

  1. A dashboard was built, with charts to display the number of feeds/events generated in the past 7 days by my TTRSS and huginn services (more services to come).
    1. On 31 July, a new graph which displays wangqiru/ttrss docker image’s pull counts is added. This utilises node-cron, axios and sequelize.
  2. SCSS was learned with practice, a style is only stylish when it’s reusable.

  3. Experience in using Gulp was gained, which will save me some time in future building.

  4. More node.js practices were done while developing the backend. Also bugs in existing code were discovered and fixed along the way. Improvements were also implemented due to richer node.js experience.

The end product looks easy to produce but the journey is what makes the destination beautiful.

Last updated