Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
C-D-Lewis committed Oct 16, 2024
1 parent 937300c commit 3473b56
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 26 deletions.
48 changes: 27 additions & 21 deletions dashboard2/src/components/AppArea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AppState } from '../types';
import StatRow from './StatRow';
import AppLoader from './AppLoader';
import AppCard from './AppCard';
import DeviceMetrics from './DeviceMetrics';

declare const fabricate: Fabricate<AppState>;

Expand Down Expand Up @@ -38,28 +39,33 @@ const AppCardList = () => fabricate('Row')
*
* @returns {HTMLElement} Fabricate component.
*/
const AppArea = () => fabricate('Column')
.setStyles({ width: '100%' })
.onUpdate(async (el, state) => {
const { selectedDevice } = state;
const AppArea = () => {
/**
* Determine if this device's apps are loaded.
*
* @param {AppState} s - App state.
* @returns {boolean} true if apps are loaded
*/
const areAppsLoaded = (s: AppState) => !!s.selectedDeviceApps?.length;

if (!selectedDevice) {
el.setChildren([NoDeviceLabel()]);
return;
}
return fabricate('Column')
.setStyles({ width: '100%' })
.onUpdate(async (el, state) => {
const { selectedDevice } = state;

setTimeout(() => el.scrollIntoView({ behavior: 'smooth' }), 200);
el.setChildren([
StatRow({ device: selectedDevice }),
fabricate.conditional(
(s) => !s.selectedDeviceApps?.length,
AppLoader,
),
fabricate.conditional(
(s) => !!s.selectedDeviceApps?.length,
() => AppCardList(),
),
]);
}, [fabricate.StateKeys.Created, 'selectedDevice']);
if (!selectedDevice) {
el.setChildren([NoDeviceLabel()]);
return;
}

setTimeout(() => el.scrollIntoView({ behavior: 'smooth' }), 200);
el.setChildren([
StatRow({ device: selectedDevice }),
fabricate.conditional((s) => !areAppsLoaded(s), AppLoader),
fabricate.conditional(areAppsLoaded, DeviceMetrics),
fabricate.conditional(areAppsLoaded, AppCardList),
]);
}, [fabricate.StateKeys.Created, 'selectedDevice']);
};

export default AppArea;
2 changes: 1 addition & 1 deletion dashboard2/src/components/AppCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const AppCard = ({ app }: { app: DeviceApp }) => {

return fabricate('Column')
.setStyles(({ palette }) => ({
margin: '10px',
margin: '15px',
backgroundColor: palette.grey3,
width: '300px',
border: `solid 2px ${palette.grey6}`,
Expand Down
154 changes: 154 additions & 0 deletions dashboard2/src/components/DeviceMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Fabricate, FabricateComponent } from 'fabricate.js';
import {
AppState, DataPoint, MetricData, MetricName,
} from '../types';
import { fetchMetric } from '../util';
import Theme from '../theme';

declare const fabricate: Fabricate<AppState>;

/**
* NoDeviceLabel component.
*
* @returns {HTMLElement} Fabricate component.
*/
const NoMetricsLabel = () => fabricate('Text')
.setStyles(({ palette }) => ({
color: palette.grey5,
cursor: 'default',
height: '100px',
backgroundColor: palette.grey2,
width: '100%',
textAlign: 'center',
alignContent: 'center',
}))
.setNarrowStyles({
margin: '0px',
})
.setText('No metrics to show');

/**
* MetricGraph component.
*
* @param {object} props - Component props.
* @param {MetricName} props.name - Metric name to fetch and graph.
* @returns {FabricateComponent} MetricGraph component.
*/
const MetricGraph = ({ name } : { name: MetricName }) => {
const dataKey = fabricate.buildKey('metricData', name);
const canvas = fabricate('canvas') as unknown as HTMLCanvasElement;

/**
* Draw metric data on the canvas.
*
* @param {AppState} state - App state.
*/
const draw = (state: AppState) => {
const { width, height } = canvas;
const { buckets, minValue, maxValue } = state[dataKey] as MetricData;

const range = maxValue - minValue;

const ctx = canvas.getContext('2d')!;

// Background
ctx.fillStyle = Theme.palette.grey2;
ctx.fillRect(0, 0, width, height);

if (!buckets.length) {
// ctx.font = '24px Arial';
// ctx.fillStyle = 'white';
// ctx.fillText('No data', width / 2, height / 2);
return;
}

// Latest data
ctx.fillStyle = Theme.palette.secondary;
const points = buckets.length > width ? buckets.slice(buckets.length - width) : buckets;
points.forEach((p: DataPoint, i: number) => {
const pHeight = ((p.value - minValue) * height) / range;
const x = i;
const y = height - pHeight;

ctx.fillRect(x, y, 2, 2);
});
};

return fabricate('div')
.setStyles(({ palette }) => ({
width: '100%',
height: '100%',
}))
.setChildren([canvas as unknown as FabricateComponent<AppState>])
.onUpdate(async (el, state, keys) => {
if (keys.includes(fabricate.StateKeys.Created)) {
fetchMetric(state, name);
return;
}

if (keys.includes(dataKey)) {
canvas.width = el.offsetWidth - 1;
canvas.height = el.offsetHeight;

draw(state);
}
}, [fabricate.StateKeys.Created, dataKey]);
};

/**
* MetricContainer component.
*
* @param {object} props - Component props.
* @param {MetricName} props.name - Metric name to fetch and graph.
* @returns {FabricateComponent} MetricContainer component.
*/
const MetricContainer = ({ name } : { name: MetricName }) => fabricate('Column')
.setStyles(({ palette }) => ({
margin: '15px',
width: '30%',
height: '180px',
border: `solid 2px ${palette.grey6}`,
}))
.setNarrowStyles({
width: '100%',
})
.setChildren([
fabricate('Text')
.setStyles(({ fonts, palette }) => ({
color: 'white',
fontSize: '0.9rem',
fontFamily: fonts.code,
margin: '5px 0px 0px 0px',
padding: '0px 5px',
borderBottom: `solid 2px ${palette.grey6}`,
}))
.setText(name),
MetricGraph({ name }),
]);

/**
* DeviceMetrics component.
*
* @returns {HTMLElement} Fabricate component.
*/
const DeviceMetrics = () => fabricate('Row')
.setStyles({
margin: '15px',
flexWrap: 'wrap',
})
.onUpdate(async (el, state) => {
const { selectedDevice } = state;

if (!selectedDevice) {
el.setChildren([NoMetricsLabel()]);
return;
}

el.setChildren([
MetricContainer({ name: 'cpu' }),
MetricContainer({ name: 'memoryPerc' }),
MetricContainer({ name: 'tempRaw' }),
]);
}, [fabricate.StateKeys.Created, 'selectedDevice']);

export default DeviceMetrics;
11 changes: 8 additions & 3 deletions dashboard2/src/components/SideBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ const GroupLabel = ({ publicIp }: { publicIp: string }) => fabricate('Text')
* @returns {HTMLElement} Fabricate component.
*/
const DeviceRow = ({ device }: { device: Device }) => {
const { deviceType, deviceName, localIp } = device;
const {
deviceType, deviceName, localIp, lastCheckIn,
} = device;

const nameView = fabricate('Text')
.setStyles({
Expand All @@ -60,12 +62,16 @@ const DeviceRow = ({ device }: { device: Device }) => {
*/
const isSelected = (s: AppState) => s.selectedDevice?.deviceName === deviceName;

const minsAgo = Math.round((Date.now() - lastCheckIn) / (1000 * 60));
const seenRecently = minsAgo < 12; // Based on default checkin interval of 10m

return fabricate('Row')
.setStyles(({ palette }) => ({
backgroundColor: palette.grey3,
padding: '4px 0px 4px 8px',
cursor: 'pointer',
borderBottom: `solid 2px ${palette.grey6}`,
borderLeft: `solid 5px ${seenRecently ? palette.statusOk : palette.grey6}`,
}))
.setChildren([
fabricate('Image', { src: `assets/images/${ICON_NAMES[deviceType]}.png` })
Expand Down Expand Up @@ -105,8 +111,7 @@ const DeviceRow = ({ device }: { device: Device }) => {
const SideBar = () => fabricate('Column')
.setStyles(({ palette }) => ({
backgroundColor: palette.grey3,
minWidth: '240px',
borderRight: `solid 2px ${palette.grey6}`,
minWidth: '250px',
}))
.setNarrowStyles({ width: '100vw' })
.onUpdate(async (el, state) => {
Expand Down
2 changes: 2 additions & 0 deletions dashboard2/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const CONDUIT_PORT = 5959;
export const INITIAL_STATE: AppState = {
// App data
token: '',

// Loaded data
selectedDeviceApps: [],
devices: [],

Expand Down
1 change: 1 addition & 0 deletions dashboard2/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const Theme = {
palette: {
primary: '#c7053d',
secondary: '#ffeb3b',
text: 'white',
grey1: '#111',
grey2: '#222',
Expand Down
24 changes: 24 additions & 0 deletions dashboard2/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,37 @@ export type Device = {
diskUsage: number;
};

/** Graph data point. */
export type DataPoint = {
value: number;
timestamp: number;
dateTime: string;
};

/** Available graphed metrics */
export type MetricName = 'cpu' | 'memoryPerc' | 'tempRaw';

/** Raw metric point */
export type MetricPoint = [number, number];

/** Metric data */
export type MetricData = {
buckets: DataPoint[];
minTime: string;
maxTime: string;
minValue: number;
maxValue: number;
};

/** App state type */
export type AppState = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;

// App data
token: string;

// Loaded data
selectedDeviceApps: DeviceApp[];
devices: Device[];

Expand Down
Loading

0 comments on commit 3473b56

Please sign in to comment.