diff --git a/dashboard2/src/components/DeviceMetrics.ts b/dashboard2/src/components/DeviceMetrics.ts index 82274b1..bbdb379 100644 --- a/dashboard2/src/components/DeviceMetrics.ts +++ b/dashboard2/src/components/DeviceMetrics.ts @@ -1,10 +1,10 @@ import { Fabricate, FabricateComponent } from 'fabricate.js'; import { - AppState, DataPoint, MetricData, MetricName, + AppState, DataPoint, MetricData, } from '../types'; -import { fetchMetric } from '../util'; import Theme from '../theme'; import { BUCKET_SIZE } from '../constants'; +import { fetchMetric, sendConduitPacket } from '../services/conduitService'; declare const fabricate: Fabricate; @@ -13,9 +13,12 @@ const LABEL_OFFSET = 3; /** Graph width based on length of a day */ const GRAPH_WIDTH = Math.round(1440 / BUCKET_SIZE); /** Map of friendly metric names */ -const METRIC_NAME_MAP = { +const METRIC_NAME_MAP: Record = { cpu: 'CPU', memoryPerc: 'Memory (%)', + memoryMb: 'Memory (MB)', + diskGb: 'Disk (GB)', + discPerc: 'Disk (%)', tempRaw: 'Temperature', freqPerc: 'CPU Frequency (%)', }; @@ -51,10 +54,10 @@ type PlotPoint = { * MetricGraph component. * * @param {object} props - Component props. - * @param {MetricName} props.name - Metric name to fetch and graph. + * @param {string} props.name - Metric name to fetch and graph. * @returns {FabricateComponent} MetricGraph component. */ -const MetricGraph = ({ name } : { name: MetricName }) => { +const MetricGraph = ({ name } : { name: string }) => { const dataKey = fabricate.buildKey('metricData', name); const canvas = fabricate('canvas') as unknown as HTMLCanvasElement; @@ -155,10 +158,10 @@ const MetricGraph = ({ name } : { name: MetricName }) => { * MetricContainer component. * * @param {object} props - Component props. - * @param {MetricName} props.name - Metric name to fetch and graph. + * @param {string} props.name - Metric name to fetch and graph. * @returns {FabricateComponent} MetricContainer component. */ -const MetricContainer = ({ name } : { name: MetricName }) => fabricate('Column') +const MetricContainer = ({ name } : { name: string }) => fabricate('Column') .setStyles(({ palette }) => ({ margin: '15px', width: `${GRAPH_WIDTH}px`, @@ -179,7 +182,7 @@ const MetricContainer = ({ name } : { name: MetricName }) => fabricate('Column') padding: '5px', borderBottom: `solid 2px ${palette.grey6}`, })) - .setText(METRIC_NAME_MAP[name]), + .setText(METRIC_NAME_MAP[name] || name), MetricGraph({ name }), ]); @@ -193,20 +196,29 @@ const DeviceMetrics = () => fabricate('Row') margin: '15px', flexWrap: 'wrap', }) - .onUpdate(async (el, state) => { - const { selectedDevice } = state; + .onCreate(async (el, state) => { + // Get the metrics available, each graph loads its own + fabricate.update({ metricNames: [] }); + const { + message: metricNames, + } = await sendConduitPacket(state, { to: 'monitor', topic: 'getMetricNames' }); + fabricate.update({ metricNames }); + }) + .onUpdate(async (el, state, keys) => { + const { selectedDevice, metricNames } = state; if (!selectedDevice) { el.setChildren([NoMetricsLabel()]); return; } - el.setChildren([ - MetricContainer({ name: 'cpu' }), - MetricContainer({ name: 'memoryPerc' }), - MetricContainer({ name: 'tempRaw' }), - MetricContainer({ name: 'freqPerc' }), - ]); - }, [fabricate.StateKeys.Created, 'selectedDevice']); + if (keys.includes('metricNames')) { + el.setChildren( + metricNames + .filter((name) => !!METRIC_NAME_MAP[name]) + .map((name) => MetricContainer({ name })), + ); + } + }, [fabricate.StateKeys.Created, 'selectedDevice', 'metricNames']); export default DeviceMetrics; diff --git a/dashboard2/src/components/SideBar.ts b/dashboard2/src/components/SideBar.ts index 60032e5..bd429c7 100644 --- a/dashboard2/src/components/SideBar.ts +++ b/dashboard2/src/components/SideBar.ts @@ -1,8 +1,9 @@ import { Fabricate } from 'fabricate.js'; import { AppState, Device } from '../types'; -import { fetchDeviceApps, sortDeviceByName } from '../util'; +import { sortDeviceByName } from '../util'; import { ICON_NAMES } from '../constants'; import AppLoader from './AppLoader'; +import { fetchDeviceApps } from '../services/conduitService'; declare const fabricate: Fabricate; @@ -87,9 +88,6 @@ const DeviceRow = ({ device }: { device: Device }) => { })); }) .onClick((el, state) => { - const { selectedDevice } = state; - if (selectedDevice?.deviceName === deviceName) return; - // Select this device fabricate.update({ selectedDevice: device }); diff --git a/dashboard2/src/constants.ts b/dashboard2/src/constants.ts index 16c7c3b..093ecd1 100644 --- a/dashboard2/src/constants.ts +++ b/dashboard2/src/constants.ts @@ -14,6 +14,7 @@ export const INITIAL_STATE: AppState = { // Loaded data selectedDeviceApps: [], devices: [], + metricNames: [], // Selections selectedDevice: null, diff --git a/dashboard2/src/index.ts b/dashboard2/src/index.ts index 31ccd9c..91c2702 100644 --- a/dashboard2/src/index.ts +++ b/dashboard2/src/index.ts @@ -2,9 +2,10 @@ import { Fabricate } from 'fabricate.js'; import { AppState } from './types'; import Theme from './theme'; import { INITIAL_STATE } from './constants'; -import { parseParams, fetchFleetList } from './util'; +import { parseParams } from './util'; import SideBar from './components/SideBar'; import AppArea from './components/AppArea'; +import { fetchFleetList } from './services/conduitService'; declare const fabricate: Fabricate; diff --git a/dashboard2/src/services/conduitService.ts b/dashboard2/src/services/conduitService.ts index ce76d2e..eb73817 100644 --- a/dashboard2/src/services/conduitService.ts +++ b/dashboard2/src/services/conduitService.ts @@ -1,5 +1,15 @@ -import { CONDUIT_PORT } from '../constants'; -import { AppState, Packet } from '../types'; +import { Fabricate } from 'fabricate.js'; +import { + AppState, DataPoint, Device, DeviceApp, + Packet, +} from '../types'; +import { BUCKET_SIZE, CONDUIT_PORT, FLEET_HOST } from '../constants'; +import { shortDateTime, sortAppByName } from '../util'; + +declare const fabricate: Fabricate; + +/** Extra Y for visibility */ +const Y_EXTRA = 1.1; /** * Send a conduit packet. @@ -54,3 +64,153 @@ export const sendConduitPacket = async ( throw error; } }; + +/** + * Re-load the devices list data. + * + * @param {object} state - App state. + */ +export const fetchFleetList = async (state: AppState) => { + const { token } = state; + fabricate.update({ devices: [] }); + + try { + // Can't use sendConduitPacket, not a device by name + const res = await fetch(`http://${FLEET_HOST}:${CONDUIT_PORT}/conduit`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + to: 'attic', + topic: 'get', + message: { app: 'conduit', key: 'fleetList' }, + auth: token || '', + }), + }); + const { message } = await res.json(); + fabricate.update({ devices: message.value }); + } catch (err) { + console.error(err); + alert(err); + } +}; + +/** + * Load apps for all fleet devices. + * + * @param {AppState} state - App state. + * @param {Device} device - Device to use. + */ +export const fetchDeviceApps = async (state: AppState, device: Device) => { + const { deviceName } = device!; + + fabricate.update({ selectedDeviceApps: [] }); + + try { + const { message } = await sendConduitPacket( + state, + { to: 'conduit', topic: 'getApps' }, + deviceName, + ); + + let selectedDeviceApps: DeviceApp[] = []; + if (message && message.error) { + console.error(message.error); + selectedDeviceApps = []; + } else if (!message) { + console.error('No response in fetchApps'); + selectedDeviceApps = []; + } else { + selectedDeviceApps = (message as DeviceApp[]).sort(sortAppByName); + } + + fabricate.update({ selectedDeviceApps }); + } catch (err: unknown) { + console.error(err); + fabricate.update({ selectedDeviceApps: [] }); + } +}; + +/** + * Fetch data for a metric. + * + * @param {AppState} state - App state. + * @param {string} name - Metric name. + */ +export const fetchMetric = async (state: AppState, name: string) => { + const dataKey = fabricate.buildKey('metricData', name); + fabricate.update( + dataKey, + { + buckets: [], + minTime: 0, + maxTime: 0, + minValue: 0, + maxValue: 0, + }, + ); + + const res = await sendConduitPacket( + state, + { + to: 'monitor', + topic: 'getMetricToday', + message: { name }, + }, + ); + const { message: newHistory } = res; + if (res.error) console.log(res); + if (!newHistory.length) return; + + const type = Array.isArray(newHistory[0][1]) ? 'array' : 'number'; + + const minTime = shortDateTime(newHistory[0][0]); + const maxTime = shortDateTime(newHistory[newHistory.length - 1][0]); + let minValue = 0; + let maxValue = 0; + + if (type === 'number') { + // Aggregate values + minValue = name.includes('Perc') + ? 0 + : newHistory.reduce( + // @ts-expect-error handled with 'type' + (acc: number, [, value]: MetricPoint) => (value < acc ? value : acc), + 9999999, + ); + maxValue = name.includes('Perc') + ? Math.round(100 * Y_EXTRA) + : Math.round( + newHistory.reduce( + // @ts-expect-error handled with 'type' + (acc: number, [, value]: MetricPoint) => (value > acc ? value : acc), + 0, + ) * Y_EXTRA, + ); + } else { + throw new Error('Unexpected metric data type'); + } + + // Average into buckets + const copy = [...newHistory]; + const buckets: DataPoint[] = []; + while (copy.length) { + const points = copy.splice(0, BUCKET_SIZE); + const avgIndex = Math.floor(points.length / 2); + buckets.push({ + value: points.reduce((acc, [, value]) => acc + value, 0) / points.length, + timestamp: points[avgIndex][0], + dateTime: new Date(points[avgIndex][0]).toISOString(), + }); + } + + fabricate.update( + dataKey, + { + buckets, + minTime, + maxTime, + minValue, + maxValue, + }, + ); +}; diff --git a/dashboard2/src/types.ts b/dashboard2/src/types.ts index ce20c77..3ad2001 100644 --- a/dashboard2/src/types.ts +++ b/dashboard2/src/types.ts @@ -38,9 +38,6 @@ export type DataPoint = { dateTime: string; }; -/** Available graphed metrics */ -export type MetricName = 'cpu' | 'memoryPerc' | 'tempRaw' | 'freqPerc'; - /** Raw metric point */ export type MetricPoint = [number, number]; @@ -64,6 +61,7 @@ export type AppState = { // Loaded data selectedDeviceApps: DeviceApp[]; devices: Device[]; + metricNames: string[]; // Selections selectedDevice: Device | null, diff --git a/dashboard2/src/util.ts b/dashboard2/src/util.ts index a806d97..67fd6f7 100644 --- a/dashboard2/src/util.ts +++ b/dashboard2/src/util.ts @@ -1,44 +1,11 @@ import { Fabricate, FabricateComponent } from 'fabricate.js'; import { - AppState, DataPoint, Device, DeviceApp, MetricName, + AppState, Device, DeviceApp, } from './types'; -import { BUCKET_SIZE, CONDUIT_PORT, FLEET_HOST } from './constants'; import { sendConduitPacket } from './services/conduitService'; declare const fabricate: Fabricate; -/** Extra Y for visibility */ -const Y_EXTRA = 1.1; - -/** - * Re-load the devices list data. - * - * @param {object} state - App state. - */ -export const fetchFleetList = async (state: AppState) => { - const { token } = state; - fabricate.update({ devices: [] }); - - try { - // Can't use sendConduitPacket, not a device by name - const res = await fetch(`http://${FLEET_HOST}:${CONDUIT_PORT}/conduit`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - to: 'attic', - topic: 'get', - message: { app: 'conduit', key: 'fleetList' }, - auth: token || '', - }), - }); - const { message } = await res.json(); - fabricate.update({ devices: message.value }); - } catch (err) { - console.error(err); - alert(err); - } -}; - /** * Parse query params. */ @@ -128,43 +95,7 @@ export const commandDevice = async ( * @param {Device} b - Device to compare. * @returns {number} Sort ordering. */ -const sortAppByName = (a: DeviceApp, b: DeviceApp) => (a.app! > b.app! ? 1 : -1); - -/** - * Load apps for all fleet devices. - * - * @param {AppState} state - App state. - * @param {Device} device - Device to use. - */ -export const fetchDeviceApps = async (state: AppState, device: Device) => { - const { deviceName } = device!; - - fabricate.update({ selectedDeviceApps: [] }); - - try { - const { message } = await sendConduitPacket( - state, - { to: 'conduit', topic: 'getApps' }, - deviceName, - ); - - let selectedDeviceApps: DeviceApp[] = []; - if (message && message.error) { - console.error(message.error); - selectedDeviceApps = []; - } else if (!message) { - console.error('No response in fetchApps'); - selectedDeviceApps = []; - } else { - selectedDeviceApps = (message as DeviceApp[]).sort(sortAppByName); - } - - fabricate.update({ selectedDeviceApps }); - } catch (err: unknown) { - console.error(err); - fabricate.update({ selectedDeviceApps: [] }); - } -}; +export const sortAppByName = (a: DeviceApp, b: DeviceApp) => (a.app! > b.app! ? 1 : -1); /** * Shorter representation of a date time. @@ -177,88 +108,3 @@ export const shortDateTime = (timestamp: string) => { const shortTime = time.split(':').slice(0, 2).join(':'); return `${shortTime}`; }; - -/** - * Fetch data for a metric. - * - * @param {AppState} state - App state. - * @param {MetricName} name - Metric name. - */ -export const fetchMetric = async (state: AppState, name: MetricName) => { - const dataKey = fabricate.buildKey('metricData', name); - fabricate.update( - dataKey, - { - buckets: [], - minTime: 0, - maxTime: 0, - minValue: 0, - maxValue: 0, - }, - ); - - const res = await sendConduitPacket( - state, - { - to: 'monitor', - topic: 'getMetricToday', - message: { name }, - }, - ); - const { message: newHistory } = res; - if (res.error) console.log(res); - if (!newHistory.length) return; - - const type = Array.isArray(newHistory[0][1]) ? 'array' : 'number'; - - const minTime = shortDateTime(newHistory[0][0]); - const maxTime = shortDateTime(newHistory[newHistory.length - 1][0]); - let minValue = 0; - let maxValue = 0; - - if (type === 'number') { - // Aggregate values - minValue = name.includes('Perc') - ? 0 - : newHistory.reduce( - // @ts-expect-error handled with 'type' - (acc: number, [, value]: MetricPoint) => (value < acc ? value : acc), - 9999999, - ); - maxValue = name.includes('Perc') - ? Math.round(100 * Y_EXTRA) - : Math.round( - newHistory.reduce( - // @ts-expect-error handled with 'type' - (acc: number, [, value]: MetricPoint) => (value > acc ? value : acc), - 0, - ) * Y_EXTRA, - ); - } else { - throw new Error('Unexpected metric data type'); - } - - // Average into buckets - const copy = [...newHistory]; - const buckets: DataPoint[] = []; - while (copy.length) { - const points = copy.splice(0, BUCKET_SIZE); - const avgIndex = Math.floor(points.length / 2); - buckets.push({ - value: points.reduce((acc, [, value]) => acc + value, 0) / points.length, - timestamp: points[avgIndex][0], - dateTime: new Date(points[avgIndex][0]).toISOString(), - }); - } - - fabricate.update( - dataKey, - { - buckets, - minTime, - maxTime, - minValue, - maxValue, - }, - ); -}; diff --git a/node-common/src/modules/log.js b/node-common/src/modules/log.js index a6f20ed..bb60ce2 100644 --- a/node-common/src/modules/log.js +++ b/node-common/src/modules/log.js @@ -169,7 +169,6 @@ const getLogfileSizeMb = () => { const { size } = fs.statSync(getFilePath()); return Math.round(size / (1024 * 1024)); } catch (e) { - // OK, not there yet return undefined; } }; @@ -178,6 +177,9 @@ const getLogfileSizeMb = () => { * Monitor the log size, and erase if it gets too big. */ const monitorLogSize = () => { + // Initally clear + fs.unlinkSync(getFilePath()); + setInterval(() => { const sizeMb = getLogfileSizeMb(); info(`Logfile size: ${sizeMb} MB`);