Mustard is an awesome low-code data dashboard written in Go, TypeScript and VueJS. Mustard uses CSS Grid layout hence only supports modern browsers. Mustard updates the widgets real-time using Server Sent Events (SSE) and has out of box support for Kafka Streams, S3 buckets among others.
Mustard has 4 components:
- UI widgets - written in VueJS and TypeScript
- Go Jobs - these are backend jobs which execute on a schedule to get the data
- A Kafka topic consumer
- Server Sent Events for pushing data to the UI widgets
- Mustard relies on dotenv for setting configuration values like API keys etc. There's a sample .env.example file which needs to be renamed as .env with all keys set.
- Mustard relies on cron to fire events on schedule but also exposes an API to fire jobs immediately (POST /api/nudge). This API is used by the Vue client to get the data on first run instead of waiting for the events to fire on schedule.
- Since v0.2 Mustard also supports consuming Kafka topics and forwarding those events over Server Sent Events. More details are under Kafka section.
- Mustard supports multiple dashboards which are laidout in
config/config.json
. Each dashboard should have a unique id and is accessible over/dashboard/{id}
URL. - Starting v0.7.0 Mustard supports configuration in Yaml format in
config/config.yml
. This takes precedence over json file if both are found in the directory.
$ cd <<folder>>/client
$ yarn install
$ yarn prod
$ cd ../
$ go mod download
$ go build
$ ./mustard
Navigate to http://localhost:8090/dashboard/{id}. Default port is 8090 unless overridden.
Below is how the folder structure looks like:
├───client
│ ├───node_modules
│ └───src
│ ├───assets
│ ├───components
│ ├───eventsink
│ └───store
└───jobs
Folders of interest are:
- client/src/components - All dashboard widgets should be in this folder.
- jobs/ - All Go jobs should be in this folder.
Few widgets and jobs are already included in repository - feel free to use/abuse them
- Text widget - displays a title and subtitle
- Slideshow widget - cycles through images on an interval. This is currently wired up with a job to get flickr images
- Clock widget - displays 3 clocks and a trivia on today's day
- A comparer widget - Displays a stacked bar graph comparing 2 values. If the difference between the values is > pre-defined threshold then entire widget turns red else stays green. Currently it display the # of Covid-19 cases in india for T+0 and T-1.
- Weather widget - displays weather from OpenWeather and gets the backrground image from flickr for the weather condition
- List widget - cycles through a list of items (title, image and description) on schedule
Jobs schedule is set in config.json
file in /config folder.:
{
"jobs": [
{ "name": "name-of-job", "schedule": "schedule-of-job" },
{ "name": "weather", "schedule": "@every 1h" }
]
}
This allows all jobs to have their job schedule configurable. Any job which is not present in config is disabled. (since 0.3)
- Blogroll - gets RSS feed from a site and converts the URL into a QR Code.
- Weather - gets the current weather from OpenWeather
- Number - gets the number trivia for today's day
- comparison - gets today and yesterday's Covid-19 cases
- FlickrShow - gets images from flickr
- S3Show - gets images from a S3 bucket filtered by a prefix (aka folder).
- Use any of the job as reference, move any configuration field to .env. You can read the env var using
os.Getenv("VAR")
- The job schedule can be defined in human readble form like "every nm|h|d"
- Call
mustardcore.GetEventsManager().Notify(data)
to notify the dashboard. eventId in data is what ties this data to a particular widget. Below is an example of how number job pushes data to clockWidget
data := mustardcore.EventData{Event: "clockWidget", Data: number{Trivia: string(text)}}
mustardcore.GetEventsManager().Notify(data)
Mustard supports POST data to /api/webhook
as a passthrough, the post body should be of JSON form {"event": "string", "data": object }
. This requires passing the API_KEY
defined in env as base64 encoded value as X_API_KEY
header.
$ curl --location --request POST '<mustard-url>/api/webhook' \
--header 'Content-Type: application/json' \
--header 'X_API_KEY: <base64encoded_api_key>' \
--data-raw '{"event": "slideshow", "data": ...}'
Instead of hard-wiring the layout in vue file, the layout is retrieved from layout
API. The API should return layout JSON data array in this form:
type layoutType = {
component: string;
class: string | string[];
props: Record<string, unknown>;
state?: Record<string, unknown>;
};
The properties are:
- component - The name of the component to render for e.g.
WeatherWidget
- class string | [] - classes to apply to this component.
column
is always added. - props - any props to pass to this widget, generally if it is a vuex backed component, eventId should be passed for e.g.
{ eventId: "weather" }
- state - the state module definition, should be the default state for this widget in the form of
state: { weather: {} }
. The name of the key (weather) is used as name of the module (no namespacing).
The view passes the current pathname fragment to this API thus allowing having multiple dashboards. The current implementation of API gets the layout from config.json
file stored in /config folder.
- Make a copy of TextWidget.vue and use that as reference
- Import the widget in App.vue and add it in the grid:
<div class="column">
<WeatherWidget eventId="weather" />
</div>
- Widgets use VueX for state management, each widget defines their own module for state. The modules are registered dynamically instead of having to add empty definitions.
- eventId prop ties this widget with SSE data
- Retrieving model/SSE from state is passed as module-name and 'data'. It can be easily retrieved in widget by making use of
getStoreItem()
helper function in store. Something like:
import { getStoreItem, State } from "../store";
// ....
get numberTrivia(): { trivia: string } {
return (
getStoreItem((this.$store.state as unknown) as State, this.eventId) || {
trivia: "Hello world"
}
);
}
- widget width and height can be adjusted by setting classes x{1..4} and y{1..4} respectively.
- Starting v0.2, a shared animated number widget component is added. This component can be used to animate a changing number value. The widget is same as one provided in Vue state animation example.
Apart from the regular way of cloning and modifying, mustard supports extensions via gists. The gists can be installed by providing the ID of the gist $ ./mustard -gist={id}
. Only .vue and .go files are supported in the gist. Sample gists
widget | Description |
---|---|
Mopidy Client | A Simple client for Mopidy which displays now playing |
Animated Number | Animates a number and displays up/down arrow |
YouTube Video | Plays a YouTube in loop (muted) |
- From v0.2, Mustard starts a kafka listener by connecting to broker URL specified in .env and subscribing to topic in .env. Multiple Broker URLs in .env should be separated by comma. Mustard kafka listener is primarily a proxy between the widget and the kafka topic, it does not add any intelligence on top of the message received and just forwards it over the SSE. Message published on kafka topic should have same JSON structure as expected by SSE:
type EventData struct {
Event string `json:"event"`
Data interface{} `json:"data"`
}
Kafka topics are great for pushing NRT metrices (Near Real Time) to the dashboard. Kafka topic bound widgets can be used to display Time series graphs or volatile values. Mustard already has built-in support for ApexCharts, which can be used for displaying charts.
There's a docker compose file which exposes kafka listener both internally and externally. On windows, since traffic cannot be routed to linux containers, the kafka listener is exposed as host.docker.internal on the host. The docker-compose for windows is docker-compose-win.yml.
$ docker-compose -f ./docker-compose.yml up -d
Please make sure that mustard's env file has the topic you need to listen to (KAFKA_TOPIC)
The docker compose file uses https://github.com/wurstmeister/kafka-docker/, please refer to the documentation there to troubleshoot connectivity issues.
On windows, this is how it would potentially work:
- Create the topic:
.\kafka-topics.bat --bootstrap-server host.docker.internal:9094 --topic test --create
- Set the topic name in .env
- Produce a message:
.\kafka-console-producer.bat --bootstrap-server host.docker.internal:9094 --topic test
$ docker pull goavega/mustard:latest
$ docker run -p <local>:80 --env-file ./.env goavega/mustard
Starting v0.6 Mustard supports making Restful API calls without writing any jobs by using Restful under the hood. Restful also supports basic json transformation to transform response into Mustard events. API url, body and authorization header support string interpolation with env variables. The OOBE config.json
has 2 APIs wired up with text widget as example.
For more information please read Restful Readme.
- Create wiki
- Drag and drop support
- Data persistence
- Shared Chart components
- API Piping to chain multiple API calls
- Add sample dashboards
Q. Why is it called Mustard?
A. Good question - we like using color names.
Q. Why Go and Vue, and not React and Node or X & Y?
A. This was primarily used as a weekend project to learn something new. Go and Vue seemed to be good choices to learn over the weekend :).
Q. How is it currently used?
A. Currently Mustard runs on desk on a repurposed screen from a bricked laptop and raspberry pi.
MIT. Inspired by Dashing.