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