diff --git a/web/src/components/measure-summaries/ListSummary.vue b/web/src/components/measure-summaries/ListSummary.vue new file mode 100644 index 0000000..525c279 --- /dev/null +++ b/web/src/components/measure-summaries/ListSummary.vue @@ -0,0 +1,6 @@ + + diff --git a/web/src/components/MeasureSummary.vue b/web/src/components/measure-summaries/MeasureSummary.vue similarity index 52% rename from web/src/components/MeasureSummary.vue rename to web/src/components/measure-summaries/MeasureSummary.vue index d21d044..1d1836e 100644 --- a/web/src/components/MeasureSummary.vue +++ b/web/src/components/measure-summaries/MeasureSummary.vue @@ -1,7 +1,10 @@ diff --git a/web/src/components/measure-summaries/SimpleSummaryGraph.vue b/web/src/components/measure-summaries/SimpleSummaryGraph.vue new file mode 100644 index 0000000..778f513 --- /dev/null +++ b/web/src/components/measure-summaries/SimpleSummaryGraph.vue @@ -0,0 +1,4 @@ + + diff --git a/web/src/components/measure-summaries/list-summary.ts b/web/src/components/measure-summaries/list-summary.ts new file mode 100644 index 0000000..9b46fbf --- /dev/null +++ b/web/src/components/measure-summaries/list-summary.ts @@ -0,0 +1,16 @@ +import { Component, Prop, Vue } from 'vue-property-decorator'; +import { Measure, ListMeasureConfig, Measurement, ListMeasurementMeta } from '@/models'; + +@Component +export class ListSummary extends Vue { + @Prop() private measure!: Measure; + @Prop() private measurements!: Array>; + + private top5(): Array> { + return this.measurements + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) + .slice(0, 5); + } +} + +export default ListSummary; diff --git a/web/src/components/measure-summary.scss b/web/src/components/measure-summaries/measure-summary.scss similarity index 100% rename from web/src/components/measure-summary.scss rename to web/src/components/measure-summaries/measure-summary.scss diff --git a/web/src/components/measure-summaries/measure-summary.ts b/web/src/components/measure-summaries/measure-summary.ts new file mode 100644 index 0000000..85aad98 --- /dev/null +++ b/web/src/components/measure-summaries/measure-summary.ts @@ -0,0 +1,26 @@ +import { Component, Prop, Vue } from 'vue-property-decorator'; +import { Measure, MeasureConfig, MeasureType, Measurement, MeasurementMeta } from '@/models'; +import { measurementStore } from '@/store'; +import ListSummary from './ListSummary.vue'; +import SimpleSummaryGraph from './SimpleSummaryGraph.vue'; + +@Component({ + components: { + ListSummary, + SimpleSummaryGraph + } +}) +export class MeasureSummary extends Vue { + @Prop() private measure!: Measure; + + private get measurements() { + return measurementStore.measurements[this.measure.id] || []; + } + + private async mounted() { + await measurementStore.fetchMeasurements(this.measure); + } + +} + +export default MeasureSummary; diff --git a/web/src/components/measure-summaries/simple-summary-graph.ts b/web/src/components/measure-summaries/simple-summary-graph.ts new file mode 100644 index 0000000..bb9a170 --- /dev/null +++ b/web/src/components/measure-summaries/simple-summary-graph.ts @@ -0,0 +1,28 @@ +import { Component, Prop, Vue } from 'vue-property-decorator'; +import { Measure, MeasureConfig, Measurement, MeasurementMeta } from '@/models'; + +@Component +export class SimpleSummaryGraph extends Vue { + @Prop() private measure!: Measure; + @Prop() private measurements!: Array>; + + private chartOptions = { + chart: { sparkline: { enabled: true } }, + grid: { padding: { top: 20 }}, + stroke: { curve: 'smooth' }, + noData: { text: 'no data', + style: { fontSize: '18px' } }, + xaxis: { type: 'datetime' } + }; + + private get measurementData(): ApexAxisChartSeries { + const measurementData = this.measurements || []; + + return [{ + name: this.measure.name, + data: measurementData.map((m) => ({ x: m.timestamp.toISOString(), y: m.value })) + }]; + } +} + +export default SimpleSummaryGraph; diff --git a/web/src/components/measure-summary.ts b/web/src/components/measure-summary.ts deleted file mode 100644 index 1b7f5ba..0000000 --- a/web/src/components/measure-summary.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Component, Prop, Vue } from 'vue-property-decorator'; -import { Measure, MeasureConfig, Measurement, MeasurementMeta } from '@/models'; -import { measurementStore } from '@/store'; - -@Component -export class MeasureSummary extends Vue { - @Prop() private measure!: Measure; - - private chartOptions = { - chart: { sparkline: { enabled: true } }, - grid: { padding: { top: 20 }}, - stroke: { curve: 'smooth' } - }; - - private chartData = [ - { name: 'Test', data: [1, 10, 4, 6, 2] } - ]; - - private measurements: Array> = []; - - private mounted() { - this.measurements = - } - -} - -export default MeasureSummary; diff --git a/web/src/models.d.ts b/web/src/models.d.ts index 1a93c3c..937a9bf 100644 --- a/web/src/models.d.ts +++ b/web/src/models.d.ts @@ -1,4 +1,4 @@ -export enum MeasureType { Simple } +export enum MeasureType { List = 'list', Simple = 'simple' } export interface ApiToken { id: string; @@ -15,9 +15,11 @@ export interface LoginSubmit { } export interface MeasureConfig { - type: string; + type: MeasureType; } +export interface ListMeasureConfig extends MeasureConfig { } + export interface Measure { id: string; userId: string; @@ -27,7 +29,13 @@ export interface Measure { config: C; } -export interface MeasurementMeta {} +export interface MeasurementMeta { + measureType: MeasureType; +} + +export interface ListMeasurementMeta extends MeasurementMeta { + entry: string; +} export interface Measurement { id: string; diff --git a/web/src/services/pm-api-client.ts b/web/src/services/pm-api-client.ts index 9f0abec..169741a 100644 --- a/web/src/services/pm-api-client.ts +++ b/web/src/services/pm-api-client.ts @@ -1,7 +1,7 @@ import { default as Axios, AxiosInstance } from 'axios'; +import assign from 'lodash.assign'; import { ApiToken, LoginSubmit, Measure, MeasureConfig, Measurement, MeasurementMeta, User } from '@/models'; import { Logger, logService } from '@/services/logging'; -import merge from 'lodash.merge'; export class PmApiClient { private http: AxiosInstance; @@ -132,16 +132,18 @@ export class PmApiClient { : Promise>> { const resp = await this.http.get(`/measure/${measureSlug}`); - return resp.data; + return resp.data.map(this.fromMeasurementDTO); } - public async createMeasurement( + public async createMeasurement( measureSlug: string, - measurement: Measurement) - : Promise> { + measurement: Measurement) + : Promise> { - const resp = await this.http.post(`/measure/${measureSlug}`, measurement); - return resp.data; + const resp = await this.http.post( + `/measure/${measureSlug}`, + this.toMeasurementDTO(measurement)); + return this.fromMeasurementDTO(resp.data); } public async getMeasurement( @@ -151,7 +153,7 @@ export class PmApiClient { const resp = await this.http .get(`/measure/${measureSlug}/${measurementId}`); - return resp.data; + return this.fromMeasurementDTO(resp.data); } public async updateMeasurement( @@ -159,9 +161,10 @@ export class PmApiClient { measurement: Measurement) : Promise> { - const resp = await this.http - .put(`/measure/${measureSlug}/${measurement.id}`, measurement); - return resp.data; + const resp = await this.http.put( + `/measure/${measureSlug}/${measurement.id}`, + this.toMeasurementDTO(measurement)); + return this.fromMeasurementDTO(resp.data); } public async deleteMeasurement( @@ -173,6 +176,15 @@ export class PmApiClient { .delete(`/measure/${measureSlug}/${measurementId}`); return true; } + + private fromMeasurementDTO(dto: any): Measurement { + return assign({}, dto, { timestamp: new Date(dto.timestamp) }); + } + + private toMeasurementDTO(measurement: Measurement): object { + return assign({}, measurement, { timestamp: measurement.timestamp.toISOString() }); + } + } export const api = new PmApiClient(process.env.VUE_APP_PM_API_BASE); diff --git a/web/src/store-modules/measure.ts b/web/src/store-modules/measure.ts index a8a8caa..7afeaed 100644 --- a/web/src/store-modules/measure.ts +++ b/web/src/store-modules/measure.ts @@ -9,23 +9,21 @@ import { import { keyBy } from 'lodash'; import { User, Measure, MeasureConfig } from '@/models'; import api from '@/services/pm-api-client'; -import { logService } from '@/services/logging'; @Module({ namespaced: true, name: 'measure' }) export class MeasureStoreModule extends VuexModule { public measures: { [key: string]: Measure } = {}; - private log = logService.getLogger('/store-modules/measure'); - @MutationAction({ mutate: ['measures'], rawError: true }) public async fetchAllMeasures() { const measures = await api.getAllMeasures(); return { measures: keyBy(measures, 'slug') }; } - @Action({ commit: 'SET_MEASURE', rawError: true }) + @Action({ rawError: true }) public async fetchMeasure(slug: string) { - return await api.getMeasure(slug); + const measure = api.getMeasure(slug); + this.context.commit('SET_MEASURE', measure); } @Mutation private SET_MEASURE(measure: Measure) { diff --git a/web/src/store-modules/measurement.ts b/web/src/store-modules/measurement.ts index 5531e58..7bdea4b 100644 --- a/web/src/store-modules/measurement.ts +++ b/web/src/store-modules/measurement.ts @@ -3,19 +3,37 @@ import { getModule, Module, Mutation, + MutationAction, VuexModule } from 'vuex-module-decorators'; -import { Measurement, MeasurementMeta } from '@/models'; +import findIndex from 'lodash.findindex'; +import { Measure, MeasureConfig, Measurement, MeasurementMeta } from '@/models'; +import assign from 'lodash.assign'; import api from '@/services/pm-api-client'; +import { logService } from '@/services/logging'; + +export interface MeasurementStore { [key: string]: Array>; } +export interface SetMeasurementsParameters { + measure: Measure; + measurements: Array>; +} + +const logger = logService.getLogger('/store-modules/measurement'); @Module({ namespaced: true, name: 'measurement' }) export class MeasurementStoreModule extends VuexModule { - public measurements: { [key: string]: Array> } = {}; + public measurements: MeasurementStore = {}; - @Action({ commit: 'SET_MEASUREMENTS', rawError: true }) - public async getchMeasurements(measureSlug: string) { - return await api.getMeasurements(measureSlug); + @Action({ rawError: true }) + public async fetchMeasurements(measure: Measure) { + logger.debug('Fetching measurements for measure ' + measure.id); + const measurements = await api.getMeasurements(measure.slug); // assumption: always returns at least [] + this.context.commit('SET_MEASUREMENTS', { measure, measurements }); + } + + @Mutation + public SET_MEASUREMENTS({ measure: measure, measurements: measurements }: SetMeasurementsParameters) { + this.measurements = assign({}, this.measurements, { [measure.id]: measurements }); } - // @Mutation private SET_MEASUREMENTS( } diff --git a/web/src/store.ts b/web/src/store.ts index 27993f6..02543a4 100644 --- a/web/src/store.ts +++ b/web/src/store.ts @@ -17,7 +17,7 @@ export const store = new Vuex.Store({ apiToken: ApiTokenStoreModule, auth: AuthStoreModule, measure: MeasureStoreModule, - measurements: MeasurementStoreModule, + measurement: MeasurementStoreModule, user: UserStoreModule } }); diff --git a/web/src/types/apexcharts-inner-types.d.ts b/web/src/types/apexcharts-inner-types.d.ts new file mode 100644 index 0000000..7cf989a --- /dev/null +++ b/web/src/types/apexcharts-inner-types.d.ts @@ -0,0 +1,7 @@ +declare module 'apexcharts-types' { + export interface ApexAxisChartOneSeries { + name: string; + data: number[] | Array<{ x: string; y: number }>; + } + export type ApexAxisChartSeries = ApexAxisChartOneSeries[]; +} diff --git a/web/src/views/measures.ts b/web/src/views/measures.ts index 747476b..e391136 100644 --- a/web/src/views/measures.ts +++ b/web/src/views/measures.ts @@ -1,5 +1,5 @@ import { Component, Vue } from 'vue-property-decorator'; -import MeasureSummary from '@/components/MeasureSummary.vue'; +import MeasureSummary from '@/components/measure-summaries/MeasureSummary.vue'; import Test from '@/components/Test.vue'; import { measureStore } from '@/store';