Mozaik 🤘

(mozaik.rocks)

 

How to create TV dashboard for devs

Przemek Suchodolski / @przemuh / przemuh.pl

  • @przemuh / przemuh.pl

  • As a dev since 2012

  • 🇰🇷 korpo, 🇵🇱 software-house, 🇺🇸 startup

  • Musician amateur

  • "Gamer" / PS4

About me

History

Old dashboard in dashing

Delphi-TV on jenkins

Brainstorming

  • build status
  • merge requests list
  • sprint status
  • burn-down chart
  • tests results
  • number of escalations
  • ...

Requirements

  • easy to setup
  • easy to maintain
  • easy to extend

aka 3 x easy

RESEARCH

Extensions (22)

  • mozaik-ext-github
  • mozaik-ext-gitlab
  • mozaik-ext-slack
  • mozaik-ext-travis
  • mozaik-ext-time
  • mozaik-ext-weather
  • mozaik-ext-jenkins
  • mozaik-ext-minio
  • mozaik-ext-teamcity
  • mozaik-ext-heroku
  • mozaik-ext-analytics
  • mozaik-ext-switch
  • mozaik-ext-embed
  • mozaik-ext-iframe
  • mozaik-ext-calendar
  • mozaik-ext-json
  • mozaik-ext-multijson
  • mozaik-ext-value
  • mozaik-ext-bamboo
  • mozaik-ext-image
  • mozaik-ext-okrs
  • mozaik-ext-saucelabs

mozaik-ext ...

80

ARCHITECTURE

server

SPA app

client

widgets

config.yaml

api poll interval

WS

Extensions:

Before moving forward

v1

  • stable
  • install & go
  • more ext
  • react - ^0.13.3

  • react-mixins

  • sass

  • poor DX

  • good UX

v2

  • latest
  • install & fix & fix & go
  • less ext
  • latest react

  • composition

  • css-in-js (styled-components)

  • great DX (live-reload)

  • better UX

Coding time!

Getting started

git clone git@github.com:plouc/mozaik-demo.git
cd mozaik-demo
git checkout mozaik-2
npm install
npm start

Getting started

git clone git@github.com:plouc/mozaik-demo.git
cd mozaik-demo
git checkout mozaik-2
npm install
npm start

Getting started

git clone git@github.com:plouc/mozaik-demo.git
cd mozaik-demo
git checkout mozaik-2
npm install
npm start

Getting started

git clone git@github.com:plouc/mozaik-demo.git
cd mozaik-demo
git checkout mozaik-2
npm install
npm start

First problem

Compiled successfully!

You can now view mozaik-demo in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://192.168.43.16:3000/

Note that the development build is not optimized.
To create a production build, use yarn build.

Proxy error: Could not proxy request /config from localhost:3000 to http://localhost:5000.
See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (ECONNREFUSED).

[HPM] Error occurred while trying to proxy request /config from localhost:3000 to http://localhost:5000 (ECONNREFUSED) (https://nodejs.org/api/errors.html#errors_common_system_errors)

NEVER GIVE UP

Starting a server

$ node server.js
internal/modules/cjs/loader.js:584
    throw err;
    ^

Error: Cannot find module 'dotenv'
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:582:15)
    at Function.Module._load (internal/modules/cjs/loader.js:508:25)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:22:18)
    at Object.<anonymous> (/Users/psuchodolski/Dev/github/mozaik-demo/server.js:1:63)
    at Module._compile (internal/modules/cjs/loader.js:701:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
    at Module.load (internal/modules/cjs/loader.js:600:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
    at Function.Module._load (internal/modules/cjs/loader.js:531:3)

NEVER GIVE UP

Missing deps

$ npm install dotenv@6 request
> using config file: 'conf/config.yml'

info: Registered API 'mozaik' (mode: poll)
info: serving static contents from /Users/psuchodolski/Dev/github/mozaik-demo/build
info: Mozaïk server started on port 5000

Server is running

but...

     "@mozaik/themes": "1.0.0-alpha.16",
     "@mozaik/ui": "^2.0.0-alpha.15",
+    "dotenv": "^6.2.0",
     "nivo": "^0.15.0",
-    "react": "^15.6.1",
-    "react-dom": "^15.6.1"

+    "react": "^16.8.6",
+    "react-dom": "^16.8.6",
+    "request": "^2.88.0"

   },
   "devDependencies": {
     "react-scripts": "1.0.10"

error: [github] github.repositoryContributorsStats.plouc/mozaik - status code: 403
error: [travis] travis.repository.plouc.mozaik - status code: 403
error: [travis] travis.repositoryBuildHistory.plouc.mozaik.10 - status code: 403
error: [travis] travis.repositoryBuildHistory.plouc.mozaik.20 - status code: 403
error: [github] github.branches.plouc/mozaik - status code: 403
error: [github] github.user.plouc - status code: 403
error: [github] github.pullRequests.plouc/mozaik - status code: 403
error: [github] github.repository.plouc/mozaik - status code: 403
error: [github] github.organization.ekino - status code: 403
error: [github] github.status - status code: 404

Configuration

                         columns: 3
+——————————————————+——————————————————+——————————————————+
|                  |                                     |
| A -> x: 0  y: 0  |        B -> x: 1  y: 0              |
|      columns: 1  |             columns: 2              |
|      rows:    1  |             rows:    1              |
|                  |                                     |
+——————————————————+——————————————————+——————————————————+  rows: 2
|                                     |                  |
|        C -> x: 0  y: 1              | D -> x: 2  y: 1  |
|             columns: 2              |      columns: 1  |
|             rows:    1              |      rows:    1  |
|                                     |                  |
+——————————————————+——————————————————+——————————————————+

Grid system

port: 8888
host: 10.0.2.35

rotationDuration: 15

apis:
  pollInterval: 30000

dashboards:
- columns: 4
  rows:    8
  title:   Main
  widgets:
  -
    extension:    monitoring
    widget:       Versions
    columns:      4
    rows:         2
    x:            0
    y:            0
  -
    extension:    jenkins
    widget:       JobStatus
    job:          delphi-platform-bazel/job/develop
    title:        Develop
    columns:      1
    rows:         3
    x:            0
    y:            2
port: 8888
host: 10.0.2.35

rotationDuration: 15

apis:
  pollInterval: 30000

dashboards:
- columns: 4
  rows:    8
  title:   Main
  widgets:
  -
    extension:    monitoring
    widget:       Versions
    columns:      4
    rows:         2
    x:            0
    y:            0
  -
    extension:    jenkins
    widget:       JobStatus
    job:          delphi-platform-bazel/job/develop
    title:        Develop
    columns:      1
    rows:         3
    x:            0
    y:            2
port: 8888
host: 10.0.2.35

rotationDuration: 15

apis:
  pollInterval: 30000

dashboards:
- columns: 4
  rows:    8
  title:   Main
  widgets:
  -
    extension:    monitoring
    widget:       Versions
    columns:      4
    rows:         2
    x:            0
    y:            0
  -
    extension:    jenkins
    widget:       JobStatus
    job:          delphi-platform-bazel/job/develop
    title:        Develop
    columns:      1
    rows:         3
    x:            0
    y:            2
port: 8888
host: 10.0.2.35

rotationDuration: 15

apis:
  pollInterval: 30000

dashboards:
- columns: 4
  rows:    8
  title:   Main
  widgets:
  -
    extension:    monitoring
    widget:       Versions
    columns:      4
    rows:         2
    x:            0
    y:            0
  -
    extension:    jenkins
    widget:       JobStatus
    job:          delphi-platform-bazel/job/develop
    title:        Develop
    columns:      1
    rows:         3
    x:            0
    y:            2
port: 8888
host: 10.0.2.35

rotationDuration: 15

apis:
  pollInterval: 30000

dashboards:
- columns: 4
  rows:    8
  title:   Main
  widgets:
  -
    extension:    monitoring
    widget:       Versions
    columns:      4
    rows:         2
    x:            0
    y:            0
  -
    extension:    jenkins
    widget:       JobStatus
    job:          delphi-platform-bazel/job/develop
    title:        Develop
    columns:      1
    rows:         3
    x:            0
    y:            2

Ext registration

// index.js

import React from "react";
import ReactDOM from "react-dom";
import "./register_themes";
import "./register_extensions";
import Mozaik from "@mozaik/ui";

ReactDOM.render(<Mozaik />, document.getElementById("root"));

UI Entry Point

// index.js

import React from "react";
import ReactDOM from "react-dom";
import "./register_themes";
import "./register_extensions";
import Mozaik from "@mozaik/ui";

ReactDOM.render(<Mozaik />, document.getElementById("root"));

UI Entry Point

// index.js

import React from "react";
import ReactDOM from "react-dom";
import "./register_themes";
import "./register_extensions";
import Mozaik from "@mozaik/ui";

ReactDOM.render(<Mozaik />, document.getElementById("root"));

UI Entry Point

import { Registry } from "@mozaik/ui";

import monitoring from "./ext-monitoring/components";
import jenkins from "./ext-jenkins/components";
import jira from "./ext-jira/components";
import gitlab from "./ext-gitlab/components";

Registry.addExtensions({
  monitoring,
  jenkins,
  jira,
  gitlab,
});

Register Extensions

import { Registry } from "@mozaik/ui";

import monitoring from "./ext-monitoring/components";
import jenkins from "./ext-jenkins/components";
import jira from "./ext-jira/components";
import gitlab from "./ext-gitlab/components";

Registry.addExtensions({
  monitoring,
  jenkins,
  jira,
  gitlab,
});

Register Extensions

import { Registry } from "@mozaik/ui";

import monitoring from "./ext-monitoring/components";
import jenkins from "./ext-jenkins/components";
import jira from "./ext-jira/components";
import gitlab from "./ext-gitlab/components";

Registry.addExtensions({
  monitoring,
  jenkins,
  jira,
  gitlab,
});

Register Extensions

import { Registry } from "@mozaik/ui";

import monitoring from "./ext-monitoring/components";
import jenkins from "./ext-jenkins/components";
import jira from "./ext-jira/components";
import gitlab from "./ext-gitlab/components";

Registry.addExtensions({
  monitoring,
  jenkins,
  jira,
  gitlab,
});

Register Extensions

import Versions from "./Versions";
import FailingScans from "./FailingScans";

export default {
  Versions,
  FailingScans,
};

Components export

extension:    monitoring
widget:       Versions
columns:      4
rows:         2
x:            0
y:            0

Simple Widget

import React, { Component } from "react";
import { TrapApiError, Widget, WidgetLoader } from "@mozaik/ui";

class Versions extends Component {

  static getApiRequest() {
    return {
      id: "monitoring.deployedVersions",
    };
  }

  render() {
    const { apiData, apiError } = this.props;

    const body = apiData 
        ? <div>{ apiData.join(",") }</div>
        : <WidgetLoader />;

    return (
      <Widget>
        <TrapApiError error={apiError}>
            {body}
        </TrapApiError>
      </Widget>
    );
  }
}

export default Versions;
import React, { Component } from "react";
import { TrapApiError, Widget, WidgetLoader } from "@mozaik/ui";

class Versions extends Component {

  static getApiRequest() {
    return {
      id: "monitoring.deployedVersions",
    };
  }

  render() {
    const { apiData, apiError } = this.props;

    const body = apiData 
        ? <div>{ apiData.join(",") }</div>
        : <WidgetLoader />;

    return (
      <Widget>
        <TrapApiError error={apiError}>
            {body}
        </TrapApiError>
      </Widget>
    );
  }
}

export default Versions;
import React, { Component } from "react";
import { TrapApiError, Widget, WidgetLoader } from "@mozaik/ui";

class Versions extends Component {

  static getApiRequest() {
    return {
      id: "monitoring.deployedVersions",
    };
  }

  render() {
    const { apiData, apiError } = this.props;

    const body = apiData 
        ? <div>{ apiData.join(",") }</div>
        : <WidgetLoader />;

    return (
      <Widget>
        <TrapApiError error={apiError}>
            {body}
        </TrapApiError>
      </Widget>
    );
  }
}

export default Versions;
import React, { Component } from "react";
import { TrapApiError, Widget, WidgetLoader } from "@mozaik/ui";

class Versions extends Component {

  static getApiRequest() {
    return {
      id: "monitoring.deployedVersions",
    };
  }

  render() {
    const { apiData, apiError } = this.props;

    const body = apiData 
        ? <div>{ apiData.join(",") }</div>
        : <WidgetLoader />;

    return (
      <Widget>
        <TrapApiError error={apiError}>
            {body}
        </TrapApiError>
      </Widget>
    );
  }
}

export default Versions;
import React, { Component } from "react";
import { TrapApiError, Widget, WidgetLoader } from "@mozaik/ui";

class Versions extends Component {

  static getApiRequest() {
    return {
      id: "monitoring.deployedVersions",
    };
  }

  render() {
    const { apiData, apiError } = this.props;

    const body = apiData 
        ? <div>{ apiData.join(",") }</div>
        : <WidgetLoader />;

    return (
      <Widget>
        <TrapApiError error={apiError}>
            {body}
        </TrapApiError>
      </Widget>
    );
  }
}

export default Versions;

API client

require("dotenv").load({ silent: true });

const path = require("path");
const Mozaik = require("@mozaik/server").default;

let configFile = process.argv[2] || "conf/config.yml";

console.log(`> using config file: '${configFile}'\n`);

Mozaik.configureFromFile(path.join(__dirname, configFile))
  .then((config) => {
    require("./src/register_apis")(Mozaik, configFile, config);
    Mozaik.start();
  })
  .catch((err) => {
    console.error(err);
  });

Server Entry Point

require("dotenv").load({ silent: true });

const path = require("path");
const Mozaik = require("@mozaik/server").default;

let configFile = process.argv[2] || "conf/config.yml";

console.log(`> using config file: '${configFile}'\n`);

Mozaik.configureFromFile(path.join(__dirname, configFile))
  .then((config) => {
    require("./src/register_apis")(Mozaik, configFile, config);
    Mozaik.start();
  })
  .catch((err) => {
    console.error(err);
  });

Server Entry Point

require("dotenv").load({ silent: true });

const path = require("path");
const Mozaik = require("@mozaik/server").default;

let configFile = process.argv[2] || "conf/config.yml";

console.log(`> using config file: '${configFile}'\n`);

Mozaik.configureFromFile(path.join(__dirname, configFile))
  .then((config) => {
    require("./src/register_apis")(Mozaik, configFile, config);
    Mozaik.start();
  })
  .catch((err) => {
    console.error(err);
  });

Server Entry Point

require("dotenv").load({ silent: true });

const path = require("path");
const Mozaik = require("@mozaik/server").default;

let configFile = process.argv[2] || "conf/config.yml";

console.log(`> using config file: '${configFile}'\n`);

Mozaik.configureFromFile(path.join(__dirname, configFile))
  .then((config) => {
    require("./src/register_apis")(Mozaik, configFile, config);
    Mozaik.start();
  })
  .catch((err) => {
    console.error(err);
  });

Server Entry Point

Register API

module.exports = (Mozaik /* configFile, config */) => {
  Mozaik.registerApi("monitoring", require("./ext-monitoring/client"));
  Mozaik.registerApi("jenkins", require("./ext-jenkins/client"));
  Mozaik.registerApi("jira", require("./ext-jira/client"));
  Mozaik.registerApi("gitlab", require("./ext-gitlab/client"));
};

Register API

module.exports = (Mozaik /* configFile, config */) => {
  Mozaik.registerApi("monitoring", require("./ext-monitoring/client"));
  Mozaik.registerApi("jenkins", require("./ext-jenkins/client"));
  Mozaik.registerApi("jira", require("./ext-jira/client"));
  Mozaik.registerApi("gitlab", require("./ext-gitlab/client"));
};

Client code

const chalk = require("chalk");

module.exports = (mozaik) => {
    return {
        deployedVersions() {
            const options = {
                uri: "https://yourAPI.dot.com",
                json: true
            };

            mozaik.logger.info(chalk.yellow("[monitoring] fetching data"));

            return mozaik.request.get(options)
                .then(apiData => {
                    mozaik.logger.info(
                        chalk.green("[monitoring] fetching success")
                    );
                })
                .catch(error => {
                    mozaik.logger.error(
                        chalk.red(`[monitoring] ${error.error}`)
                    );
                    throw error;
                })
        }
    }
}

Client code

const chalk = require("chalk");

module.exports = (mozaik) => {
    return {
        deployedVersions() {
            const options = {
                uri: "https://yourAPI.dot.com",
                json: true
            };

            mozaik.logger.info(chalk.yellow("[monitoring] fetching data"));

            return mozaik.request.get(options)
                .then(apiData => {
                    mozaik.logger.info(
                        chalk.green("[monitoring] fetching success")
                    );
                })
                .catch(error => {
                    mozaik.logger.error(
                        chalk.red(`[monitoring] ${error.error}`)
                    );
                    throw error;
                })
        }
    }
}

Client code

const chalk = require("chalk");

module.exports = (mozaik) => {
    return {
        deployedVersions() {
            const options = {
                uri: "https://yourAPI.dot.com",
                json: true
            };

            mozaik.logger.info(chalk.yellow("[monitoring] fetching data"));

            return mozaik.request.get(options)
                .then(apiData => {
                    mozaik.logger.info(
                        chalk.green("[monitoring] fetching success")
                    );
                })
                .catch(error => {
                    mozaik.logger.error(
                        chalk.red(`[monitoring] ${error.error}`)
                    );
                    throw error;
                })
        }
    }
}

Client code

const chalk = require("chalk");

module.exports = (mozaik) => {
    return {
        deployedVersions() {
            const options = {
                uri: "https://yourAPI.dot.com",
                json: true
            };

            mozaik.logger.info(chalk.yellow("[monitoring] fetching data"));

            return mozaik.request.get(options)
                .then(apiData => {
                    mozaik.logger.info(
                        chalk.green("[monitoring] fetching success")
                    );
                })
                .catch(error => {
                    mozaik.logger.error(
                        chalk.red(`[monitoring] ${error.error}`)
                    );
                    throw error;
                })
        }
    }
}

Client code

const chalk = require("chalk");

module.exports = (mozaik) => {
    return {
        deployedVersions() {
            const options = {
                uri: "https://yourAPI.dot.com",
                json: true
            };

            mozaik.logger.info(chalk.yellow("[monitoring] fetching data"));

            return mozaik.request.get(options)
                .then(apiData => {
                    mozaik.logger.info(
                        chalk.green("[monitoring] fetching success")
                    );
                })
                .catch(error => {
                    mozaik.logger.error(
                        chalk.red(`[monitoring] ${error.error}`)
                    );
                    throw error;
                })
        }
    }
}

Client code

const chalk = require("chalk");

module.exports = (mozaik) => {
    return {
        deployedVersions() {
            const options = {
                uri: "https://yourAPI.dot.com",
                json: true
            };

            mozaik.logger.info(chalk.yellow("[monitoring] fetching data"));

            return mozaik.request.get(options)
                .then(apiData => {
                    mozaik.logger.info(
                        chalk.green("[monitoring] fetching success")
                    );
                })
                .catch(error => {
                    mozaik.logger.error(
                        chalk.red(`[monitoring] ${error.error}`)
                    );
                    throw error;
                })
        }
    }
}

Client code

const chalk = require("chalk");

module.exports = (mozaik) => {
    return {
        deployedVersions() {
            const options = {
                uri: "https://yourAPI.dot.com",
                json: true
            };

            mozaik.logger.info(chalk.yellow("[monitoring] fetching data"));

            return mozaik.request.get(options)
                .then(apiData => {
                    mozaik.logger.info(
                        chalk.green("[monitoring] fetching success")
                    );
                })
                .catch(error => {
                    mozaik.logger.error(
                        chalk.red(`[monitoring] ${error.error}`)
                    );
                    throw error;
                })
        }
    }
}

Client code

const chalk = require("chalk");

module.exports = (mozaik) => {
    return {
        deployedVersions() {
            const options = {
                uri: "https://yourAPI.dot.com",
                json: true
            };

            mozaik.logger.info(chalk.yellow("[monitoring] fetching data"));

            return mozaik.request.get(options)
                .then(apiData => {
                    mozaik.logger.info(
                        chalk.green("[monitoring] fetching success")
                    );
                })
                .catch(error => {
                    mozaik.logger.error(
                        chalk.red(`[monitoring] ${error.error}`)
                    );
                    throw error;
                })
        }
    }
}

Client code

const chalk = require("chalk");

module.exports = (mozaik) => {
    return {
        deployedVersions() {
            const options = {
                uri: "https://yourAPI.dot.com",
                json: true
            };

            mozaik.logger.info(chalk.yellow("[monitoring] fetching data"));

            return mozaik.request.get(options)
                .then(apiData => {
                    mozaik.logger.info(
                        chalk.green("[monitoring] fetching success")
                    );
                })
                .catch(error => {
                    mozaik.logger.error(
                        chalk.red(`[monitoring] ${error.error}`)
                    );
                    throw error;
                })
        }
    }
}

Client code

const chalk = require("chalk");

module.exports = (mozaik) => {
    return {
        deployedVersions() {
            const options = {
                uri: "https://yourAPI.dot.com",
                json: true
            };

            mozaik.logger.info(chalk.yellow("[monitoring] fetching data"));

            return mozaik.request.get(options)
                .then(apiData => {
                    mozaik.logger.info(
                        chalk.green("[monitoring] fetching success")
                    );
                })
                .catch(error => {
                    mozaik.logger.error(
                        chalk.red(`[monitoring] ${error.error}`)
                    );
                    throw error;
                })
        }
    }
}

Monitoring Versions

Our widgets

Jenkins build status

Jenkins build history

JIRA Status for QA

JIRA Escalation Tickets

JIRA Sprint status

Summary

Gains

  • Transparency
  • Quick reaction
  • More engagement
  • Starting point to discussion
  • Improvements in mozaik.rocks

Lessons learned

  • Latest !== Stable
  • Documentation is a key
  • Never give up
  • Contribute to Open Source doesn't hurt
  • Sometimes maintainers don't have time

Next steps

  • Polish widgets
  • Release them as an Open Source ext
  • New features in mozaik
  • ...maybe...someday...become a maintainer ;)

That's all

Thanks

mozaik 🤘

By Przemek Suchodolski

mozaik 🤘

mozaik.rocks

  • 554