Compare commits

..

15 Commits

Author SHA1 Message Date
824d7c59e7 fix
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2020-11-03 12:46:14 +01:00
1be8a3f592 update
All checks were successful
continuous-integration/drone/push Build is passing
2020-11-03 12:36:26 +01:00
8100a19e76 remove bezier curves
All checks were successful
continuous-integration/drone/push Build is passing
2020-11-03 12:30:27 +01:00
b6dfad0515 update
All checks were successful
continuous-integration/drone/push Build is passing
2020-11-02 18:01:57 +01:00
aa4094cd05 place start and end mark
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-13 08:35:38 +02:00
d45c8a7e9a typo
All checks were successful
continuous-integration/drone/push Build is passing
2020-09-04 17:40:29 +02:00
2ec9a9fe4d starting api spec 2020-09-04 17:40:26 +02:00
c3a11f1f3c separate settings
All checks were successful
continuous-integration/drone/push Build is passing
2020-09-02 14:34:20 +02:00
6e9d634f74 table headings
All checks were successful
continuous-integration/drone/push Build is passing
2020-09-02 12:05:48 +02:00
4174e26496 update test database 2020-09-02 12:05:37 +02:00
cf5c17bf59 styling, restructure
All checks were successful
continuous-integration/drone/push Build is passing
2020-08-30 15:28:27 +02:00
ca305dff73 move editor to bottom
All checks were successful
continuous-integration/drone/push Build is passing
2020-08-28 17:42:21 +02:00
9d91740f6e display timestamps in local timezone 2020-08-28 17:40:20 +02:00
1a75f8c242 y axis labelling 2020-08-28 17:40:00 +02:00
ca37e38800 schedule database cleanup 2020-08-28 17:39:47 +02:00
18 changed files with 9807 additions and 9458 deletions

Binary file not shown.

View File

@ -38,6 +38,9 @@ async function init (use_fake_seed) {
// await db.seed.run ({ specific: 'fake.js' }); // await db.seed.run ({ specific: 'fake.js' });
await job (db); await job (db);
setInterval (() => {
job (db);
}, 3600e3);
} }
function get_db () { function get_db () {

View File

@ -40,6 +40,8 @@ async function batch_update (knex, ids, data) {
} }
module.exports = async (knex) => { module.exports = async (knex) => {
console.log ('-- running database cleanup --');
const apps = await knex ('app') const apps = await knex ('app')
.select ('id', 'name', 'reduction'); .select ('id', 'name', 'reduction');

View File

@ -18,7 +18,7 @@
"core-js": "^3.6.5", "core-js": "^3.6.5",
"express": "^4.17.1", "express": "^4.17.1",
"express-http-proxy": "^1.6.2", "express-http-proxy": "^1.6.2",
"faker": "^4.1.0", "faker": "^5.1.0",
"knex": "^0.21.2", "knex": "^0.21.2",
"lodash.chunk": "^4.2.0", "lodash.chunk": "^4.2.0",
"sqlite3": "^5.0.0", "sqlite3": "^5.0.0",
@ -26,7 +26,7 @@
"vue-chartjs": "^3.5.0", "vue-chartjs": "^3.5.0",
"vue-router": "^3.2.0", "vue-router": "^3.2.0",
"vuex": "^3.4.0", "vuex": "^3.4.0",
"yargs": "^15.4.1" "yargs": "^16.1.0"
}, },
"devDependencies": { "devDependencies": {
"@sapphirecode/eslint-config": "^2.1.16", "@sapphirecode/eslint-config": "^2.1.16",
@ -38,7 +38,7 @@
"@vue/cli-service": "^4.4.0", "@vue/cli-service": "^4.4.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^7.5.0", "eslint": "^7.5.0",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^7.1.0",
"vue-template-compiler": "^2.6.11" "vue-template-compiler": "^2.6.11"
}, },
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",

28
spec/api.json Normal file
View File

@ -0,0 +1,28 @@
{
"openapi":"3.0.2",
"info": {
"title":"API Title",
"version":"1.0"
},
"servers": [
{"url":"https://appreports.scode.ovh/v1"}
],
"paths": {
"/app": {
"get": {
"description": "list all apps",
"parameters": [],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
}
}
}
}
}
}
}
}

View File

@ -1,34 +1,15 @@
<template> <template>
<div id="app"> <div id="app">
<p <Style />
id="build-info"
v-text="version"
/>
<router-view /> <router-view />
</div> </div>
</template> </template>
<script> <script>
import version from '../version'; import Style from './Style.vue';
export default { export default { components: { Style } };
data () {
return { version: `build: ${version}` };
}
};
</script> </script>
<style> <style scoped>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#build-info {
font-size: 10pt;
text-align: left;
}
</style> </style>

93
src/Style.vue Normal file
View File

@ -0,0 +1,93 @@
<template>
<div :class="classes" />
</template>
<script>
import Vuex from 'vuex';
export default {
computed: {
classes () {
if (this.theme !== '')
return { [this.theme]: true };
return {};
},
...Vuex.mapState ({ theme: (state) => state.theme })
}
};
</script>
<style>
:root {
/* generated with adobe color (compound) */
--Color-A: #1254B8;
--Color-B: #355585;
--Color-C: #00D8EB;
--Color-D: #F0632F;
--Color-E: #BD311E;
--Color-0: #000;
--Color-1: #222;
--color-background: var(--Color-0);
--color-background-1: var(--Color-1);
--color-foreground: var(--Color-B);
--color-contrast: var(--Color-A);
--color-high-contrast: var(--Color-C);
--color-focus: var(--Color-D);
--color-accent: var(--Color-E);
--background: var(--color-background);
--background-1: var(--color-background-1);
--container-border: 1px solid var(--color-high-contrast);
--sub-container-border: 1px solid var(--color-contrast);
--accent-border: 1px solid var(--color-accent);
--font-family: Avenir, Helvetica, Arial, sans-serif;
}
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: var(--background);
color: var(--color-foreground);
font-family: var(--font-family);
}
button {
border: var(--accent-border);
background: inherit;
color: inherit;
cursor: pointer;
transition:
border-color ease-in-out .1s,
color ease-in-out .1s;
}
button:hover {
border-color: var(--color-focus);
color: var(--color-focus);
}
input {
border: var(--accent-border);
background: inherit;
color: inherit;
}
select {
border: var(--accent-border);
background: inherit;
color: inherit;
}
select > option {
background: var(--background-1);
color: var(--color-foreground);
font-weight: bold;
}
.large_button {
padding: 10px;
}
</style>

View File

@ -1,42 +1,63 @@
<template> <template>
<div <div>
class="grid" <button
> type="button"
<ConfigEditor class="large_button"
v-model="config" @click="settings_btn_action"
:template="template" v-text="editing?'Close':'Settings'"
/>
<ViewComponent
v-for="(item,key) of saved_config.displays"
:key="key"
:config="item"
:data="log(item.source)"
/> />
<div
v-if="editing"
class="spacer"
>
<ConfigEditor
v-model="config"
:template="template"
/>
</div>
<div
v-else
class="grid"
>
<ViewComponent
v-for="(item,key) of saved_config.displays"
:key="key"
:config="item"
:data="log(item.source)"
/>
</div>
</div> </div>
</template> </template>
<script> <script>
import Vuex from 'vuex'; import Vuex from 'vuex';
import { copy_object } from '@sapphirecode/utilities'; import { copy_object } from '@sapphirecode/utilities';
import ViewComponent from '../components/ViewComponent.vue';
import ConfigEditor from '../components/ConfigEditor.vue';
import default_config from '../default'; import default_config from '../default';
import default_template from '../template'; import default_template from '../template';
import ViewComponent from './ViewComponent.vue';
import ConfigEditor from './ConfigEditor.vue';
export default { export default {
components: { ViewComponent, ConfigEditor }, components: { ViewComponent, ConfigEditor },
props: {
app: {
type: Number,
required: true
}
},
data () { data () {
return { return {
config: copy_object (default_config), config: copy_object (default_config),
saved_config: copy_object (default_config), saved_config: copy_object (default_config),
template: default_template template: default_template,
editing: false
}; };
}, },
computed: { ...Vuex.mapGetters ({ log: 'log' }) }, computed: { ...Vuex.mapGetters ({ log: 'log' }) },
mounted () { mounted () {
this.fetch_log (); this.fetch_log ();
document.body.addEventListener ('keydown', (ev) => { document.body.addEventListener ('keydown', (ev) => {
if (ev.key === 's' && ev.ctrlKey) { if (ev.key === 's' && ev.ctrlKey && this.editing) {
this.save_config (); this.save_config ();
ev.preventDefault (); ev.preventDefault ();
return false; return false;
@ -52,13 +73,20 @@ export default {
}); });
this.saved_config = copy_object (this.config); this.saved_config = copy_object (this.config);
this.fetch_log (); this.fetch_log ();
this.editing = false;
}, },
fetch_log () { fetch_log () {
this.get_log ({ this.get_log ({
app_id: this.$route.params.id, app_id: this.app,
sources: this.saved_config.sources sources: this.saved_config.sources
}); });
}, },
settings_btn_action () {
if (this.editing)
this.save_config ();
else
this.editing = true;
},
...Vuex.mapActions ({ get_log: 'get_log' }) ...Vuex.mapActions ({ get_log: 'get_log' })
} }
}; };
@ -73,7 +101,7 @@ export default {
height: 100%; height: 100%;
} }
p { .spacer {
display: inline-block; margin: 10px;
} }
</style> </style>

View File

@ -33,15 +33,19 @@ export default {
yAxisID: index, yAxisID: index,
borderColor: y.color, borderColor: y.color,
backgroundColor: y.fill, backgroundColor: y.fill,
spanGaps: true spanGaps: true,
lineTension: 0
}; };
let last = null; let last = null;
for (let i = 0; i < this.data.length; i++) { for (let i = 0; i < this.data.length; i++) {
const data = this.data[i]; const data = this.data[i];
const val = resolve_data (data, y.field); const val = resolve_data (data, y.field);
const next = this.data.length - 1 === i
? val
: resolve_data (this.data[i + 1], y.field);
if ( if (
!this.remove_duplicates !this.remove_duplicates
|| last !== val || last !== val || next !== val
|| this.data.length - 1 === i || this.data.length - 1 === i
) )
res.data.push (val); res.data.push (val);
@ -66,7 +70,16 @@ export default {
if (typeof y.max_value === 'number') if (typeof y.max_value === 'number')
range.suggestedMax = y.max_value; range.suggestedMax = y.max_value;
return { id: index, ticks: range }; return {
id: index,
ticks: range,
scaleLabel: {
labelString: y.label,
display: true,
lineHeight: 1,
padding: 0.1
}
};
} }
) )
} }

View File

@ -196,7 +196,7 @@ export default {
} }
.editor { .editor {
border: 1px solid black; border: var(--sub-container-border);
margin-left: 10px; margin-left: 10px;
grid-column: editor; grid-column: editor;
} }

View File

@ -4,6 +4,13 @@
class="table_view" class="table_view"
> >
<table> <table>
<tr v-if="headings.length > 0">
<th
v-for="(heading,key) of headings"
:key="key"
v-text="heading"
/>
</tr>
<tr <tr
v-for="(item,key) of items" v-for="(item,key) of items"
:key="key" :key="key"
@ -30,6 +37,10 @@ export default {
columns: { columns: {
type: Array, type: Array,
required: true required: true
},
headings: {
type: Array,
default: () => []
} }
}, },
methods: { resolve_data } methods: { resolve_data }
@ -38,8 +49,7 @@ export default {
<style> <style>
.table_view { .table_view {
overflow: auto; overflow-y: scoll;
border: 1px solid blue;
width: max-content; width: max-content;
max-width: 100%; max-width: 100%;
max-height: 100vh; max-height: 100vh;
@ -51,11 +61,12 @@ table {
} }
tr:nth-child(odd) { tr:nth-child(odd) {
background-color: #ddd; background-color: var(--color-background-1);
color: var(--color-accent);
} }
td { td, th {
border: 1px solid black; border: var(--sub-container-border);
margin: 0; padding: 3px;
} }
</style> </style>

View File

@ -10,6 +10,7 @@
v-else v-else
:items="data" :items="data"
:columns="config.columns" :columns="config.columns"
:headings="config.headings"
/> />
</template> </template>
@ -32,6 +33,5 @@ export default {
}; };
</script> </script>
<style> <style scoped>
</style> </style>

View File

@ -6,17 +6,10 @@
*/ */
export default { export default {
sources: [ sources: [
{ { name: 'default', limit: 140, offset: 0 },
name: 'default', { name: 'secondary', limit: 10, offset: 0 },
limit: 140, { name: 'version', limit: 1, offset: 0 }
offset: 0
},
{
name: 'secondary',
limit: 10,
offset: 0
}
], ],
displays: [ displays: [
{ {
@ -50,12 +43,31 @@ export default {
] ]
}, },
{ {
source: 'secondary', source: 'secondary',
type: 'table', type: 'table',
headings: [
'timestamp',
'temperature',
'humidity',
'light',
'fan'
],
columns: [ columns: [
'timestamp', 'timestamp',
'data' 'data/temperature',
'data/humidity',
'data/light',
'data/latch'
] ]
},
{
source: 'version',
type: 'table',
headings: [ 'hardware version' ],
columns: [ 'message' ],
x: '',
y: [],
remove_duplicates: false
} }
] ]
}; };

View File

@ -9,7 +9,6 @@
import Vue from 'vue'; import Vue from 'vue';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import AppView from '../views/AppView.vue';
import NotFound from '../views/NotFound.vue'; import NotFound from '../views/NotFound.vue';
import Home from '../views/Home.vue'; import Home from '../views/Home.vue';
@ -23,8 +22,7 @@ const routes = [
}, },
{ {
path: '/app/:id', path: '/app/:id',
name: 'AppView', redirect: '/'
component: AppView
}, },
{ {
path: '/404', path: '/404',

View File

@ -8,10 +8,12 @@
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import version from '../../version';
Vue.use (Vuex); Vue.use (Vuex);
export default new Vuex.Store ({ export default new Vuex.Store ({
state: { log: {} }, state: { log: {}, version, theme: '' },
mutations: { mutations: {
set_log (state, log) { set_log (state, log) {
state.log = log; state.log = log;
@ -41,13 +43,11 @@ export default new Vuex.Store ({
.then ((res) => res.json ()) .then ((res) => res.json ())
.then ((json) => json.map ((entry) => { .then ((json) => json.map ((entry) => {
entry.data = JSON.parse (entry.data); entry.data = JSON.parse (entry.data);
const time const time = new Date (entry.timestamp);
= (/(?<y>[0-9]+-[0-9]+-[0-9]+)T(?<t>[0-9]+:[0-9]+:[0-9]+)/u) time.setMinutes (time.getMinutes () - time.getTimezoneOffset ());
.exec ( entry.timestamp = time.toISOString ()
new Date (entry.timestamp) .replace ('T', ' ')
.toISOString () .substr (0, 19);
);
entry.timestamp = `${time.groups.y} ${time.groups.t}`;
return entry; return entry;
})); }));
logs[source.name] = log; logs[source.name] = log;

View File

@ -47,6 +47,12 @@ export default {
'chart' 'chart'
] ]
}, },
{
name: 'headings',
type: 'array',
child: { type: 'string' },
if: { prop: 'type', op: '=', val: 'table' }
},
{ {
name: 'columns', name: 'columns',
type: 'array', type: 'array',

View File

@ -1,21 +1,49 @@
<template> <template>
<ul> <div class="main_view">
<li <div class="app_list">
v-for="(app,key) in apps" <div
:key="key" v-for="(app,key) in apps"
> :key="key"
<a class="app_list_item"
:href="'/app/' + app.id" @click="active = app.id"
v-text="app.name" v-text="app.name"
/> />
</li> </div>
</ul> <div class="data_view">
<AppView
v-if="active !== null"
class="data_view_display"
:app="active"
/>
<p
v-else
class="info"
>
Select an app to view its reports
</p>
</div>
<div class="footer">
<p
class="build_info"
v-text="version"
/>
</div>
</div>
</template> </template>
<script> <script>
import Vuex from 'vuex';
import AppView from '../components/AppView.vue';
export default { export default {
components: { AppView },
data () { data () {
return { apps: [] }; return { apps: [], active: null };
},
computed: {
...Vuex.mapState (
{ version: ({ version }) => `appreports build: ${version}` }
)
}, },
async mounted () { async mounted () {
this.apps = await fetch ('/app') this.apps = await fetch ('/app')
@ -25,5 +53,53 @@ export default {
</script> </script>
<style> <style>
.main_view {
display: grid;
grid-template-areas:
'sidebar main'
'footer footer';
grid-template-columns: minmax(min-content,max-content) 1fr;
grid-template-rows: minmax(80vh, auto) 1fr;
}
.app_list {
grid-area: sidebar;
border: var(--container-border);
}
.app_list_item {
cursor: pointer;
margin: 0;
padding: 5px;
border: var(--sub-container-border);
transition: .1s color ease-in-out;
}
.app_list_item:hover {
color: var(--color-focus);
}
.app_list_item:active {
color: var(--color-accent);
}
.data_view {
grid-area: main;
border: var(--container-border);
padding: 10px;
}
.footer {
grid-area: footer;
}
.info {
color: var(--color-focus);
}
.build_info {
font-size: 10pt;
text-align: left;
}
</style> </style>

18830
yarn.lock

File diff suppressed because it is too large Load Diff