WIP Adding measurement details and entry for simple measurements.
This commit is contained in:
parent
79aa711680
commit
8114136bbd
web/src
components
measure-config
measure-details
MeasureDetails.vueSimpleDetails.vuemeasure-details.scssmeasure-details.tssimple-details.scsssimple-details.ts
measurement-entry
store-modules
views
@ -1,6 +1,6 @@
|
|||||||
import { Component, Emit, Prop, Vue, Watch } from 'vue-property-decorator';
|
import { Component, Emit, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||||
import { logService } from '@/services/logging';
|
import { logService } from '@/services/logging';
|
||||||
import { Measure, MeasureConfig, MeasureType } from '@/models';
|
import { Measure, MeasureConfig } from '@/models';
|
||||||
|
|
||||||
@Component({})
|
@Component({})
|
||||||
export class MeasureConfigForm extends Vue {
|
export class MeasureConfigForm extends Vue {
|
||||||
|
8
web/src/components/measure-details/MeasureDetails.vue
Normal file
8
web/src/components/measure-details/MeasureDetails.vue
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<SimpleDetails v-if="measure.config.type === 'simple'"
|
||||||
|
:measure=measure :measurements=measurements />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" src="./measure-details.ts"></script>
|
||||||
|
<style lang="scss" src="./measure-details.scss"></style>
|
22
web/src/components/measure-details/SimpleDetails.vue
Normal file
22
web/src/components/measure-details/SimpleDetails.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class=simple-details>
|
||||||
|
<apex-chart type="line" :options=chartOptions :series=measurementChartData />
|
||||||
|
<v-table :data=measurementTableData>
|
||||||
|
<thead slot=head >
|
||||||
|
<tr>
|
||||||
|
<v-th sortKey=tsSort defaultSort=asc >Timestamp</v-th>
|
||||||
|
<v-th sortKey=value >{{measure.name}}</v-th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody slot=body slot-scope={displayData} >
|
||||||
|
<tr v-for="row in displayData" :key="row.id">
|
||||||
|
<td>{{row.tsDisplay}}</td>
|
||||||
|
<td>{{row.value}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
<SimpleEntry :measure=measure v-model=
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" src="./simple-details.ts"></script>
|
||||||
|
<style lang="scss" src="./simple-details.scss"></style>
|
0
web/src/components/measure-details/measure-details.scss
Normal file
0
web/src/components/measure-details/measure-details.scss
Normal file
13
web/src/components/measure-details/measure-details.ts
Normal file
13
web/src/components/measure-details/measure-details.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
|
import { Measure, MeasureConfig, MeasureType, Measurement, MeasurementMeta } from '@/models';
|
||||||
|
import SimpleDetails from './SimpleDetails.vue';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: { SimpleDetails }
|
||||||
|
})
|
||||||
|
export class MeasureDetails extends Vue {
|
||||||
|
@Prop() private measure!: Measure<MeasureConfig>;
|
||||||
|
@Prop() private measurements!: Array<Measurement<MeasurementMeta>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MeasureDetails;
|
15
web/src/components/measure-details/simple-details.scss
Normal file
15
web/src/components/measure-details/simple-details.scss
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.simple-details {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
table {
|
||||||
|
th {
|
||||||
|
border-bottom: 1px black solid;
|
||||||
|
min-width: 15em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
web/src/components/measure-details/simple-details.ts
Normal file
44
web/src/components/measure-details/simple-details.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
|
import { Measure, MeasureConfig, MeasureType, Measurement, MeasurementMeta } from '@/models';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
import assign from 'lodash.assign';
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
library.add(faPencilAlt);
|
||||||
|
|
||||||
|
@Component({})
|
||||||
|
export class SimpleDetails extends Vue {
|
||||||
|
@Prop() private measure!: Measure<MeasureConfig>;
|
||||||
|
@Prop() private measurements!: Array<Measurement<MeasurementMeta>>;
|
||||||
|
|
||||||
|
private newMeasurement:
|
||||||
|
private moment = moment;
|
||||||
|
private chartOptions = {
|
||||||
|
noData: { text: 'no data',
|
||||||
|
style: { fontSize: '18px' } },
|
||||||
|
stroke: { curve: 'smooth' },
|
||||||
|
xaxis: { type: 'datetime' }
|
||||||
|
};
|
||||||
|
|
||||||
|
private get measurementChartData(): ApexAxisChartSeries {
|
||||||
|
const measurementData = this.measurements || [];
|
||||||
|
|
||||||
|
return [{
|
||||||
|
name: this.measure.name,
|
||||||
|
data: measurementData.map((m) => ({ x: m.timestamp.toISOString(), y: m.value }))
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
private get measurementTableData() {
|
||||||
|
return (this.measurements || []).map((m) => {
|
||||||
|
return assign({}, m, {
|
||||||
|
tsDisplay: moment(m.timestamp).format('MMM Do, HH:mm'),
|
||||||
|
tsSort: m.timestamp.toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SimpleDetails;
|
@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<SimpleEntry v-if="measure.config.type === 'simple'"
|
||||||
|
:measure=measure v-model=value />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" src="./measurement-entry.ts"></script>
|
||||||
|
<style lang="scss" src="./measurement-entry.scss"></style>
|
20
web/src/components/measurement-entry/SimpleEntry.vue
Normal file
20
web/src/components/measurement-entry/SimpleEntry.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<fieldset>
|
||||||
|
<div>
|
||||||
|
<label for=timestamp>Timestamp</label>
|
||||||
|
<input type=datetime-local
|
||||||
|
v-model=value.timestamp
|
||||||
|
v-show=editTimestamp
|
||||||
|
:disabled=disabled />
|
||||||
|
<span v-show="!editTimestamp">
|
||||||
|
now <a href="#" v-on:click.stop.prevent="editTimestamp = true"> (set a time)</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for=measurementValue>{{measure.name}}</label>
|
||||||
|
<input required type=number v-model=value.value :disabled=disabled />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" src="./simple-entry.ts"></script>
|
||||||
|
<style lang="scss" src="./simple-entry.scss"></style>
|
21
web/src/components/measurement-entry/measurement-entry.ts
Normal file
21
web/src/components/measurement-entry/measurement-entry.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Component, Emit, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||||
|
import { Measure, MeasureConfig, MeasureType, Measurement, MeasurementMeta } from '@/models';
|
||||||
|
import SimpleEntry from './SimpleEntry.vue';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: { SimpleEntry }
|
||||||
|
})
|
||||||
|
export class MeasurementEntry extends Vue {
|
||||||
|
@Prop() private measure!: Measure<MeasureConfig>;
|
||||||
|
@Prop() private value!: Measurement<MeasurementMeta>;
|
||||||
|
|
||||||
|
@Watch('value', { immediate: true, deep: true })
|
||||||
|
@Emit('input')
|
||||||
|
private onMeasurementChanged(newVal: Measurement<MeasurementMeta>, oldVal: Measurement<MeasurementMeta>) {
|
||||||
|
newVal.measureId = this.measure.id;
|
||||||
|
return newVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MeasurementEntry;
|
0
web/src/components/measurement-entry/simple-entry.scss
Normal file
0
web/src/components/measurement-entry/simple-entry.scss
Normal file
24
web/src/components/measurement-entry/simple-entry.ts
Normal file
24
web/src/components/measurement-entry/simple-entry.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Component, Emit, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||||
|
import { Measure, MeasureConfig, MeasureType, Measurement, MeasurementMeta } from '@/models';
|
||||||
|
|
||||||
|
@Component({})
|
||||||
|
export class SimpleEntry extends Vue {
|
||||||
|
@Prop() public measure!: Measure<MeasureConfig>;
|
||||||
|
@Prop() public value!: Measurement<MeasurementMeta>;
|
||||||
|
@Prop() public disabled: boolean = false;
|
||||||
|
private editTimestamp: boolean = false;
|
||||||
|
|
||||||
|
@Watch('value', { immediate: true, deep: true })
|
||||||
|
@Emit('input')
|
||||||
|
private onMeasurementChanged(newVal: Measurement<MeasurementMeta>, oldVal: Measurement<MeasurementMeta>) {
|
||||||
|
newVal.extData.measureType = 'simple' as MeasureType;
|
||||||
|
|
||||||
|
if (typeof(newVal.value) === 'string' ) {
|
||||||
|
newVal.value = parseInt(newVal.value, 10);
|
||||||
|
}
|
||||||
|
return newVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SimpleEntry;
|
@ -18,6 +18,11 @@ export interface SetMeasurementsParameters {
|
|||||||
measurements: Array<Measurement<MeasurementMeta>>;
|
measurements: Array<Measurement<MeasurementMeta>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MeasureAndMeasurement {
|
||||||
|
measure: Measure<MeasureConfig>;
|
||||||
|
measurement: Measurement<MeasurementMeta>;
|
||||||
|
}
|
||||||
|
|
||||||
const logger = logService.getLogger('/store-modules/measurement');
|
const logger = logService.getLogger('/store-modules/measurement');
|
||||||
|
|
||||||
@Module({ namespaced: true, name: 'measurement' })
|
@Module({ namespaced: true, name: 'measurement' })
|
||||||
@ -31,9 +36,27 @@ export class MeasurementStoreModule extends VuexModule {
|
|||||||
this.context.commit('SET_MEASUREMENTS', { measure, measurements });
|
this.context.commit('SET_MEASUREMENTS', { measure, measurements });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Action({ rawError: true })
|
||||||
|
public async createMeasurement(data: MeasureAndMeasurement) {
|
||||||
|
logger.trace('Creating new measurement for ' + data.measure.slug);
|
||||||
|
const newMeasurement = await api.createMeasurement(data.measure.slug, data.measurement);
|
||||||
|
this.context.commit('SET_MEASUREMENT', { measure: data.measure, measurement: newMeasurement });
|
||||||
|
}
|
||||||
|
|
||||||
@Mutation
|
@Mutation
|
||||||
public SET_MEASUREMENTS({ measure: measure, measurements: measurements }: SetMeasurementsParameters) {
|
public SET_MEASUREMENTS({ measure: measure, measurements: measurements }: SetMeasurementsParameters) {
|
||||||
this.measurements = assign({}, this.measurements, { [measure.id]: measurements });
|
this.measurements = assign({}, this.measurements, { [measure.id]: measurements });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation
|
||||||
|
public SET_MEASUREMENT({ measure: measure, measurement: measurement }: MeasureAndMeasurement) {
|
||||||
|
const existing = this.measurements[measure.id] || [];
|
||||||
|
const newMeasurements = existing.slice();
|
||||||
|
|
||||||
|
const index = findIndex(existing, { id: measurement.id });
|
||||||
|
if (index > 0) { newMeasurements.push(measurement); }
|
||||||
|
else { newMeasurements[index] = measurement; }
|
||||||
|
this.measurements = assign({}, this.measurements, { [measure.id]: newMeasurements });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="measure" :id="'measure-details-' + measure.slug">
|
<div v-if="measure" :id="'measure-details-' + measure.slug">
|
||||||
<div class=header-action>
|
<div class=header-action>
|
||||||
<h1>{{measure.name}}</h1>
|
<div>
|
||||||
<h2>{{measure.description}}</h2>
|
<h1>{{measure.name}}</h1>
|
||||||
|
<h2>{{measure.description}}</h2>
|
||||||
|
</div>
|
||||||
|
<router-link :to="'/new/measurement/' + measure.slug" class=btn-action>Add Measurement</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
<MeasureDetails :measure=measure :measurements=measurements />
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class=header-action>
|
<div class=header-action>
|
||||||
|
26
web/src/views/NewMeasurement.vue
Normal file
26
web/src/views/NewMeasurement.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if=measure id=new-measurement>
|
||||||
|
<div class=header>
|
||||||
|
<h1>New Measurement</h1>
|
||||||
|
<h2>Enter a new measurement for {{measure.name}}</h2>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent=createMeasurement() class=new-measurement-form>
|
||||||
|
<MeasurementEntry v-model=measurement :measure=measure :disabled=waiting />
|
||||||
|
<div v-if='!waiting' class=form-actions>
|
||||||
|
<button class=btn-action>Create </button>
|
||||||
|
<a class=btn @click="$router.go(-1)">Cancel</a>
|
||||||
|
</div>
|
||||||
|
<div v-if='waiting' class=form-waiting>
|
||||||
|
<div class=wait-spinner>working <fa-icon icon=sync spin /></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class=header-action>
|
||||||
|
<h1>There is no measure named {{$route.params.slug}}.</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" src="./new-measurement.ts"></script>
|
||||||
|
<style lang="scss" src="./new-measurement.scss"></style>
|
@ -1,9 +1,12 @@
|
|||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
import { Measure as MeasureModel, MeasureConfig, MeasureType, Measurement, MeasurementMeta } from '@/models';
|
import { Measure as MeasureModel, MeasureConfig } from '@/models';
|
||||||
import { measureStore, measurementStore } from '@/store';
|
import { measureStore, measurementStore } from '@/store';
|
||||||
|
import MeasureDetails from '@/components/measure-details/MeasureDetails.vue';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { }
|
components: {
|
||||||
|
MeasureDetails
|
||||||
|
}
|
||||||
})
|
})
|
||||||
export class Measure extends Vue {
|
export class Measure extends Vue {
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ nav {
|
|||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
padding: 0 0 1rem;
|
padding: 0 0 1rem;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .collapse-handle {
|
& > .collapse-handle {
|
||||||
|
0
web/src/views/new-measurement.scss
Normal file
0
web/src/views/new-measurement.scss
Normal file
57
web/src/views/new-measurement.ts
Normal file
57
web/src/views/new-measurement.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||||
|
import { Measure, MeasureConfig, Measurement, MeasurementMeta, MeasureType } from '@/models';
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
import { faSync } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { logService } from '@/services/logging';
|
||||||
|
import { measureStore, measurementStore } from '@/store';
|
||||||
|
import MeasurementEntry from '@/components/measurement-entry/MeasurementEntry.vue';
|
||||||
|
|
||||||
|
library.add(faSync);
|
||||||
|
|
||||||
|
const logger = logService.getLogger('/views/new-measurement');
|
||||||
|
|
||||||
|
@Component({ components: { MeasurementEntry } })
|
||||||
|
export class NewMeasurement extends Vue {
|
||||||
|
private get measure(): Measure<MeasureConfig> | null {
|
||||||
|
return measureStore.measures[this.$route.params.slug] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private waiting = false;
|
||||||
|
private measurement: Measurement<MeasurementMeta> = {
|
||||||
|
id: '',
|
||||||
|
measureId: '',
|
||||||
|
value: 0,
|
||||||
|
timestamp: new Date(),
|
||||||
|
extData: {
|
||||||
|
measureType: 'simple' as MeasureType
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private async mounted() {
|
||||||
|
if (!this.measure) {
|
||||||
|
await measureStore.fetchMeasure(this.$route.params.slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createMeasurement() {
|
||||||
|
this.waiting = true;
|
||||||
|
try {
|
||||||
|
if (this.measure) {
|
||||||
|
// Browsers that don't support datetime-local will fallback to text.
|
||||||
|
if (typeof(this.measurement.timestamp) === 'string') {
|
||||||
|
this.measurement.timestamp = new Date(this.measurement.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
await measurementStore.createMeasurement({ measure: this.measure, measurement: this.measurement});
|
||||||
|
this.$router.push({ name: 'measure', params: { slug: this.measure.slug } });
|
||||||
|
} else { /* TODO: shouldn't be possible */ }
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: show errors in the UI.
|
||||||
|
logger.error('Unable to create measurement: ' + e.message + '. \n\t' + JSON.stringify(this.measurement), e.stack);
|
||||||
|
} finally {
|
||||||
|
this.waiting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewMeasurement;
|
Loading…
x
Reference in New Issue
Block a user