Skip to content

Commit

Permalink
[WebApp] fetch earnings per machine data and pass to the chart (#1151)
Browse files Browse the repository at this point in the history
* fetch earnings per machine data and pass to the chart

* earnings per machine data handling

* fix get earnings window for line chart

* EarningLineChart: 5 initially selected machines

* url added for "Where to find the Machine ID?" link

* EarningLineChart: LIne fill prop source - improved

* EarningLineChart: loader added / empty state - handled

---------

Co-authored-by: Maksym Abramian <[email protected]>
  • Loading branch information
vitto-moz and Maksym Abramian authored Jun 27, 2024
1 parent 11318d1 commit a15e385
Show file tree
Hide file tree
Showing 9 changed files with 1,765 additions and 1,497 deletions.
216 changes: 156 additions & 60 deletions packages/web-app/src/modules/balance/BalanceStore.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
import type { AxiosInstance } from 'axios'
import { action, computed, flow, observable } from 'mobx'
import type { Guid } from 'guid-typescript'
import { action, computed, flow, observable, runInAction } from 'mobx'
import type { Moment } from 'moment'
import moment from 'moment'
import type { RootStore } from '../../Store'
import type { ChartDaysShowing, EarningWindow } from './models'
import { batchEarningsWindow, getEarningWindowsGroupedByDay } from './utils'
import type { MachinesApiClient } from '../../api/machinesApiClient/generated/machinesApiClient'
import type {
EarningHistoryTimeframeEnum,
Machine,
MachineEarningHistory,
} from '../../api/machinesApiClient/generated/models'
import { getMachinesApiClient } from '../../api/machinesApiClient/getMachinesApiClient'
import type { ChartDaysShowing, EarningPerMachine, EarningWindow } from './models'
import {
batchEarningsWindow,
getBaseKeyAsGuid,
getEarningWindowsGroupedByDay,
normalizeEarningHistory,
normalizeEarningsPerMachine,
} from './utils'

enum EarningChartTimeFilter {
Last24Hour = '24 hour filter',
Expand All @@ -12,21 +27,36 @@ enum EarningChartTimeFilter {
}

export class BalanceStore {
@observable
public currentBalance: number = 0
private machinesApiClient: MachinesApiClient

private _latestEarningsPerMachineFetchMoment: Moment | null = null

@observable
public lifetimeBalance: number = 0
private _earningsHistory: Map<number, number> = new Map()

@observable
private earningHistory: Map<number, number> = new Map()
private _earningsPerMachine: EarningPerMachine | null = {}

@observable
private daysShowingEarnings: ChartDaysShowing = 1

@observable
public currentBalance: number = 0

@observable
public lifetimeBalance: number = 0

@observable
public machines: Machine[] | null = []

@computed
public get earningsHistory(): EarningWindow[] {
return this.getEarningWindows(this.daysShowingEarnings)
return this.getEarningWindows(this.daysShowingEarnings, this._earningsHistory)
}

@computed
public get earningsPerMachine(): EarningPerMachine {
return this.getEarningWindowsPerMachine(this.daysShowingEarnings)
}

@computed
Expand Down Expand Up @@ -73,26 +103,123 @@ export class BalanceStore {
@observable
public lastMonthEarnings: number = 0

private getEarningWindows = (numberOfDays: ChartDaysShowing): EarningWindow[] => {
private getMachines = async () => {
try {
const machinesResponse = await this.machinesApiClient?.v2.machines.get()

if (machinesResponse?.items) {
return machinesResponse?.items
}
} catch (error) {
console.error('BalanceStore.getMultipleMachinesEarnings: ', error)
}

return null
}

private getEarningsPerMachine = async (machines: Machine[], chartsDaysShowing: ChartDaysShowing) => {
try {
return (
await Promise.all(
machines
.filter((machine) => machine.machine_id)
.map((machine) => {
const timeframe: EarningHistoryTimeframeEnum =
chartsDaysShowing === 1 ? '24h' : (`${chartsDaysShowing}d` as EarningHistoryTimeframeEnum)
return this.machinesApiClient?.v2.machines
.byMachine_id(getBaseKeyAsGuid(machine.machine_id as Guid))
.earningHistory.get({ queryParameters: { timeframe } })
}),
)
).filter((value) => value) as MachineEarningHistory[]
} catch (error) {
console.error('BalanceStore.getEarningsPerMachine: ', error)
return null
}
}

@action
fetchEarningsPerMachine = () => {
const shouldFetch =
this._latestEarningsPerMachineFetchMoment === null ||
moment().diff(this._latestEarningsPerMachineFetchMoment, 'hours') > 0

if (shouldFetch) {
this._latestEarningsPerMachineFetchMoment = moment()

this.getMachines()
.then((machines) => {
if (machines) {
runInAction(() => {
this.machines = machines
})
return this.getEarningsPerMachine(machines, 30)
}
throw new Error('There is no machines')
})
.then((earningsPerMachine) => {
if (earningsPerMachine) {
return runInAction(() => {
this._earningsPerMachine = normalizeEarningsPerMachine(earningsPerMachine)
})
}
throw new Error('There are no earning per machines')
})
.catch((error) => {
console.error('BalanceStore.getMultipleMachinesEarnings: ', error)
})
}
}

private getEarningWindowsPerMachine = (chartsDaysShowing: ChartDaysShowing): EarningPerMachine => {
if (this._earningsPerMachine === null) {
return {}
}
return Object.keys(this._earningsPerMachine).reduce((earningWindowsPerMachine, machineId) => {
if (!this._earningsPerMachine) {
return {}
}

const machineEarningsMap = this._earningsPerMachine[machineId]?.reduce((machineEarningsMap, item) => {
machineEarningsMap.set(item.timestamp.unix(), item.earnings)
return machineEarningsMap
}, new Map())

if (!machineEarningsMap) {
return {}
}

return {
...earningWindowsPerMachine,
[machineId]: this.getEarningWindows(chartsDaysShowing, machineEarningsMap, true),
}
}, {} as EarningPerMachine)
}

private getEarningWindows = (
chartsDaysShowing: ChartDaysShowing,
earningHistory: Map<number, number>,
isPerMachineEarning?: boolean,
): EarningWindow[] => {
const windows: EarningWindow[] = []

const now = moment.utc()

const threshold = moment(now).subtract(numberOfDays, 'days')
const threshold = moment(now).subtract(chartsDaysShowing, 'days')

let batchedEarningWindows = new Map<number, number>()
switch (numberOfDays) {
switch (chartsDaysShowing) {
case 1:
batchedEarningWindows = this.earningHistory
batchedEarningWindows = isPerMachineEarning ? batchEarningsWindow(earningHistory, 4) : earningHistory
break
case 7:
batchedEarningWindows = batchEarningsWindow(this.earningHistory, 8)
batchedEarningWindows = batchEarningsWindow(earningHistory, 8)
break
case 30:
batchedEarningWindows = batchEarningsWindow(this.earningHistory, 48)
batchedEarningWindows = batchEarningsWindow(earningHistory, 48)
break
default:
batchedEarningWindows = this.earningHistory
batchedEarningWindows = earningHistory
}

for (let [unixTime, earning] of batchedEarningWindows) {
Expand All @@ -108,16 +235,21 @@ export class BalanceStore {

const sortedEarningWindowsByTimestamp = windows.sort((a, b) => a.timestamp.diff(b.timestamp))

if (numberOfDays === 1) {
if (chartsDaysShowing === 1) {
return sortedEarningWindowsByTimestamp
}

const groupedByTheDayEarningWindows = getEarningWindowsGroupedByDay(sortedEarningWindowsByTimestamp, numberOfDays)
const groupedByTheDayEarningWindows = getEarningWindowsGroupedByDay(
sortedEarningWindowsByTimestamp,
chartsDaysShowing,
)

return groupedByTheDayEarningWindows
}

constructor(private readonly store: RootStore, private readonly axios: AxiosInstance) {}
constructor(private readonly store: RootStore, private readonly axios: AxiosInstance) {
this.machinesApiClient = getMachinesApiClient(axios)
}

@action.bound
refreshBalanceAndHistory = flow(function* (this: BalanceStore) {
Expand All @@ -143,49 +275,13 @@ export class BalanceStore {

const earningData = response.data

const roundedDown = Math.floor(moment().minute() / 15) * 15

const now = moment().minute(roundedDown).second(0).millisecond(0)

const threshold24Hrs = moment(now).subtract(24, 'hours')
const threshold7Days = moment(now).subtract(7, 'days')

const earningValues: Map<number, number> = new Map()

let total24Hrs = 0
let total7Days = 0
let total30Days = 0

//Process the input data into a usable format
for (let key in earningData) {
const timestamp = moment(key)
const earnings: number = earningData[key]
earningValues.set(timestamp.unix(), earnings)

if (timestamp >= threshold24Hrs) {
total24Hrs += earnings
}

if (timestamp >= threshold7Days) {
total7Days += earnings
}

total30Days += earnings
}

this.lastMonthEarnings = total30Days
this.lastWeekEarnings = total7Days
this.lastDayEarnings = total24Hrs

const history = new Map<number, number>()

for (let i = 0; i < 2880; ++i) {
const earning = earningValues.get(now.unix())
history.set(now.unix(), earning || 0)
now.subtract(15, 'minutes')
}
const { lastMonthEarnings, lastWeekEarnings, lastDayEarnings, earningHistory } =
normalizeEarningHistory(earningData)

this.earningHistory = history
this.lastMonthEarnings = lastMonthEarnings
this.lastWeekEarnings = lastWeekEarnings
this.lastDayEarnings = lastDayEarnings
this._earningsHistory = earningHistory
} catch (error) {
console.error('Balance history error: ')
console.error(error)
Expand Down
106 changes: 105 additions & 1 deletion packages/web-app/src/modules/balance/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Guid } from 'guid-typescript'
import { chunk, groupBy } from 'lodash'
import moment from 'moment'
import type { EarningWindow } from './models'
import type { MachineEarningHistory } from '../../api/machinesApiClient/generated/models'
import type { EarningPerMachine, EarningWindow } from './models'

export const batchEarningsWindow = (earnings: Map<number, number>, batchSize: number): Map<number, number> => {
const earningHistory = new Map<number, number>()
Expand Down Expand Up @@ -35,3 +37,105 @@ export const getEarningWindowsGroupedByDay = (

return chunkedDaysPeriodEarnings
}

export type BaseKey = string | number

export const getBaseKeyAsGuid = (key: BaseKey | Guid): Guid => {
// TODO: Do not force to string and bypass type check (after Kiota is fixed).
// https://github.com/microsoft/kiota-typescript/issues/1113
if (key instanceof Guid) {
return key.toString() as unknown as Guid
}
return Guid.parse(key.toString()).toString() as unknown as Guid
}

export const normalizeEarningsPerMachine = (
earningsPerMachine: MachineEarningHistory[] | null,
): EarningPerMachine | null => {
let normalizedEarningsPerMachine: EarningPerMachine | null = null
if (earningsPerMachine) {
normalizedEarningsPerMachine = earningsPerMachine.reduce((aggregatedEarningsPerMachine, machineEarnings) => {
if (machineEarnings.earnings) {
const machineId = machineEarnings.machine_id?.toString() as string
const { earningHistory: earningHistoryMap } = normalizeEarningHistory(
machineEarnings.earnings as Record<string, number>,
)

const machineEarningHistory = Object.fromEntries(earningHistoryMap.entries())

const earnings = Object.keys(machineEarningHistory).map((timestamp: string) => {
const timestampMoment = moment(Number(timestamp) * 1000)
return {
timestamp: timestampMoment,
earnings: machineEarningHistory[timestamp] ?? 0,
}
})

return {
...aggregatedEarningsPerMachine,
[machineId]: earnings,
}
}

return aggregatedEarningsPerMachine
}, {} as EarningPerMachine)

return normalizedEarningsPerMachine
}

return null
}

export const normalizeEarningHistory = (
earningData: Record<string, number>,
): {
lastMonthEarnings: number
lastWeekEarnings: number
lastDayEarnings: number
earningHistory: Map<number, number>
} => {
const roundedDown = Math.floor(moment().minute() / 15) * 15

const now = moment().minute(roundedDown).second(0).millisecond(0)

const threshold24Hrs = moment(now).subtract(24, 'hours')
const threshold7Days = moment(now).subtract(7, 'days')

const earningValues: Map<number, number> = new Map()

let total24Hrs = 0
let total7Days = 0
let total30Days = 0

//Process the input data into a usable format
for (let key in earningData) {
const timestamp = moment(key)
const earnings: number = earningData[key] as number
earningValues.set(timestamp.unix(), earnings)

if (timestamp >= threshold24Hrs) {
total24Hrs += earnings
}

if (timestamp >= threshold7Days) {
total7Days += earnings
}

total30Days += earnings
}

const history = new Map<number, number>()

for (let i = 0; i < 2880; ++i) {
const earning = earningValues.get(now.unix())
history.set(now.unix(), earning || 0)
now.subtract(15, 'minutes')
}

return {
lastMonthEarnings: total30Days,
lastWeekEarnings: total7Days,
lastDayEarnings: total24Hrs,
earningHistory: history,
}
}
Loading

0 comments on commit a15e385

Please sign in to comment.