Oh , of course. I've got to make it complete though. Feel free to add in a new wiki.
It needs some cleaning up probably.
note that this is currently an example WITH a build step. I am contemplating of converting this to a vanilla uibuilder project without a build step
This is a project to list/view timers (an array of objects, 1 object per timer), which have an on/off state, and have schedules attached to each of them (i.e. active at certain day(s) of the week ). Each timer runs in its own timezone , if defined, otherwise system time.
UIBuilder gets all properties of the list of timers , in the 'msg.original' object from node-red. (every minute). It also receives a list of "next" events , i.e timers that are going to change state , in the 'msg.timers'. Another requirement of the design is to make it user friendly, viewable on iOS, hence the 'large' amount of styling involved.
Main object received from node red :
msg.original (array of timers). Each timer has a property list , which is an array of schedules, and a property params, which is an array of parameters.
Below is one element of this array as an example of what is received.
[{"name":"s1","params":[{"name":"outputformat","value":0},{"name":"status","value":"1"},{"name":"location","value":""},{"name":"timezone","value":0}],"list":[{"startstop":{"Start":"10:50","Stop":"11:45"},"days":[{"name":"monday","value":true},{"name":"tuesday","value":false},{"name":"wednesday","value":true},{"name":"thursday","value":false},{"name":"friday","value":true},{"name":"saturday","value":false},{"name":"sunday","value":false}]}]},...]
Secondary object received is the list of upcoming events , all that logic in node-red , not in uibuilder.
Below is the object
[{"timer":"s5","period":30,"old_value":0,"new_value":1,"timeperiod":"21:29","state":"success"},{"timer":"s3","period":39,"old_value":1,"new_value":0,"timeperiod":"21:38","state":"danger"},{"timer":"s4","period":443,"old_value":1,"new_value":0,"timeperiod":"04:22","state":"danger"}]
it looks as follows :
File structure : :
project_dir
-- src
App.vue
index.js
index.html
-- src/views/
Home.vue
About.vue
TimeComp.vue
-- src/views/timers
Timers.vue
TimerDetails.vue
-- src/router
index.js
index.js (router) file :
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import Timers from '../views/timers/Timers.vue'
import TimerDetails from '../views/timers/TimerDetails.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/timers',
name: 'Timers',
component: Timers
},
{
path: '/timers/:id',
name: 'TimerDetails',
component: TimerDetails
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/:catchAll(.*)',
redirect: { name: 'Home'}
}
]
const router = new VueRouter({
//history: createWebHistory(process.env.BASE_URL),
mode: 'history',
base: '/uirouter',
routes
})
index.js (in src) file
import VueRouter from 'vue-router'
import router from './router'
import Vuex from 'vuex'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
Vue.use(Vuex);
Vue.use(VueRouter)
Vue.use(BootstrapVue)
Vue.use(IconsPlugin)
const store = new Vuex.Store({
state: {
timers:[{name:'s0'}],
timerevents:[]
},
mutations: {
populate (state,elem) {
state.timers = elem
},
populate_timerevents(state,elem) {
state.timerevents = elem
}
}
})
new Vue({
el: '#app',
router,
store,
render: h => h(App),
mounted: function(){
const vueApp = this
}, // --- End of mounted hook --- //
})
App.vue file
<template>
<div>
<div id="nav">
<router-link :to="{name:'Home'}">Home</router-link>|
<router-link :to="{name:'About'}">About</router-link>|
<router-link :to="{name:'Timers', params: {org:JSON.stringify(mytimers) }}">Timers</router-link>
<TimeComp/>
</div>
<router-view />
</div>
</template>
<script>
import uibuilder from './../../../node_modules/node-red-contrib-uibuilder/front-end/src/uibuilderfe.js'
import TimeComp from './views/TimeComp.vue'
export default {
data() {
return {
msgreceived:"",
mytimers:[],
}
},
created() {
uibuilder.start(this)
},
methods: {
},
mounted() {
const app = this;
uibuilder.onChange('msg', function(msg){
//console.log('Vue:mounted:UIBUILDER: property msg changed! ', msg)
app.msgreceived = msg.payload
app.mytimers = msg.original
app.timerevents = msg.timer
app.$store.commit('populate',msg.original)
app.$store.commit('populate_timerevents',msg.timer)
//console.log(app.$store.state)
}) // ---- End of uibuilder.onChange() watcher function ---- //
},
components: {
TimeComp
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
text-align: center;
font-family: Roboto;
font-size: 24px;
margin: 10px auto;
}
#nav a {
font-weight: bold;
color: #2c3e50;
text-decoration: none;
padding: 10px;
border-radius: 5px;
}
#nav a.router-link-exact-active {
color: white;
background: crimson;
}
</style>
Timer.vue
<template>
<div>
<h1 class="fade-in-image"> List </h1>
<div v-for="timer,i in example" :key="i" class="timer">
<router-link :to="{ name:'TimerDetails', params: {id:i, org:JSON.stringify(timer) }}">
<h2 >{{ timer.name }}</h2>
</router-link>
</div>
</div>
</template>
<script>
export default {
data () {
return {
mytimers: this.$store.state.timers
}
},
created() {
},
mounted() {
},
computed: {
example () {
return this.$store.state.timers
}
}
}
</script>
<style>
h1 {
margin:10px auto;
padding:10px;
text-align: center;
font-family: Roboto;
}
.timer h2 {
background: #f4f4f4;
padding: 20px;
border-radius: 15px;
margin:10px auto;
max-width: 600px;
cursor: pointer;
color: #444;
}
.timer h2:hover {
background: #ddd;
text-decoration:none!important;
}
.timer a {
text-decoration:none
}
.timer a:hover {
text-decoration:none
}
.fade-enter-from {
opacity: 0;
}
.fade-enter-to {
opacity: 1
}
.fade-enter-active {
transition: all 2s ease;
}
.fade-leave-from {
opacity: 1;
}
.fade-leave-to {
opacity: 0;
}
.fade-leave-active {
transition: all 2s ease;
}
.fade-in-image {
animation: fadein 2s;
-moz-animation: fadein 2s; /* Firefox */
-webkit-animation: fadein 2s; /* Safari and Chrome */
-o-animation: fadein 2s; /* Opera */
}
@keyframes fadein {
from {
opacity:0;
}
to {
opacity:1;
}
}
@-moz-keyframes fadein { /* Firefox */
from {
opacity:0;
}
to {
opacity:1;
}
}
@-webkit-keyframes fadein { /* Safari and Chrome */
from {
opacity:0;
}
to {
opacity:1;
}
}
@-o-keyframes fadein { /* Opera */
from {
opacity:0;
}
to {
opacity: 1;
}
}
</style>
TimerDetails.vue
<template>
<div>
<h1>
Timer <b-badge variant="danger">{{ org.name }}</b-badge>
</h1>
<div class="paramcontainer">
<div class="params">
<div class="paramitem" v-for="(param, i) in paramtable" :key="i">
<b-badge variant="light"> {{ param.name }}</b-badge>
<b-badge variant="primary"> {{ param.value }}</b-badge>
</div>
</div>
</div>
<div class="schedulecontainer">
<div class="scheduleheader">
<span class="schedheaders"
><b-badge variant="secondary">Start</b-badge></span
>
<span class="schedheaders"
><b-badge variant="secondary">Stop</b-badge></span
>
<div class="dayheaders">
<b-badge variant="secondary">Days</b-badge>
</div>
</div>
<div class="schedule" v-for="(schedule, i) in org.list" :key="i">
<b-badge variant="success"> {{ schedule.startstop.Start }}</b-badge>
<b-badge variant="danger"> {{ schedule.startstop.Stop }}</b-badge>
<div class="dayscontainer">
<div class="weekdays" v-for="(day, j) in schedule.days" :key="j">
<img
:class="(day.value && 'dayshow') || 'dayhide'"
:src="dayicons[j]"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
id: this.$route.params.id,
org: JSON.parse(this.$route.params.org),
ptable: [],
format: ["String", "Numeric", "Boolean"],
timerstatus: ["Disabled", "Enabled"],
tzformat: ["System", "Local"],
dayicons: [
"/icons8-monday-40.png",
"/icons8-tuesday-40.png",
"/icons8-wednesday-40.png",
"/icons8-thursday-40.png",
"/icons8-friday-40.png",
"/icons8-saturday-40.png",
"/icons8-sunday-40.png",
],
};
},
computed: {
paramtable() {
this.ptable.push(
{
name: "Format",
value: this.format[
this.org.params.find((el) => el.name === "outputformat").value
],
},
{
name: "Status",
value: this.timerstatus[
this.org.params.find((el) => el.name === "status").value
],
},
{
name: "Location",
value: this.org.params.find((el) => el.name === "location").value,
},
{
name: "Timezone",
value: this.tzformat[
this.org.params.find((el) => el.name === "timezone").value
],
}
);
return this.ptable;
},
},
};
</script>
<style>
.schedtable {
text-align: center;
margin: 10px auto;
max-width: 400px;
}
.schedule {
width: 25%;
margin: 0px auto;
}
.schedulecontainer {
margin-top: 40px;
}
.scheduleheader {
width: 25%;
margin: 0px auto;
font: italic 16px Roboto;
background: ghostwhite;
}
@media screen and (max-width: 600px) {
.scheduleheader,
.schedule {
width: 100%;
}
}
.schedheaders {
display: inline-block;
margin-left: 3px;
}
.dayheaders {
display: inline-block;
margin-left: 25%;
}
.dayscontainer {
display: inline-block;
}
.dayshow {
opacity: 1;
width: 100%;
}
.dayhide {
opacity: 0;
width: 100%;
}
.weekdays {
display: inline-block;
}
@media screen and (max-width: 600px) {
.weekdays {
width: 32px;
}
}
@media screen and (max-width: 600px) {
.dayshow, .dayhide {
width: 95%;
}
}
.paramitem {
display: flex;
justify-content: space-between;
padding: 1px;
}
.paramcontainer {
display: flex;
justify-content: center;
}
</style>
index.html
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NodeRED UI Builder Blank template</title>
<meta name="description" content="Node-RED UI Builder - Blank template">
<link rel="icon" href="./images/node-blue.ico">
<link type="text/css" rel="stylesheet" href="./index.css" media="all">
</head><body>
<div id="app"></div>
<!--<script src="../uibuilder/vendor/socket.io/socket.io.js"></script>-->
<!-- Note no leading / -->
<script src="./main.js" type="text/javascript"></script>
</body></html>