Add switches to static card
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.32.0",
|
"@tanstack/react-query": "^5.32.0",
|
||||||
|
"jotai": "^2.8.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
background-color: var(--color-background0);
|
background-color: var(--color-background0);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: max-content;
|
min-height: max-content;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,18 +20,17 @@
|
|||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
@media (min-height: 640px) {
|
@media (min-height: 640px) {
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
min-height: unset;
|
min-height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
@media (orientation: landscape) {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (orientation: portrait) {
|
@media (orientation: portrait) {
|
||||||
|
|||||||
4
src/client/atoms/index.ts
Normal file
4
src/client/atoms/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
|
export const highFpsAtom = atom(true);
|
||||||
|
export const siAtom = atom(false);
|
||||||
@@ -8,7 +8,6 @@ import { YAxis } from './y-axis';
|
|||||||
const stepWindow = Number(import.meta.env.CLIENT_GRAPH_STEPS);
|
const stepWindow = Number(import.meta.env.CLIENT_GRAPH_STEPS);
|
||||||
const stepPeriod = Number(import.meta.env.CLIENT_REFETCH_INTERVAL);
|
const stepPeriod = Number(import.meta.env.CLIENT_REFETCH_INTERVAL);
|
||||||
const xMargin = 4;
|
const xMargin = 4;
|
||||||
const fps = 30;
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
total: number;
|
total: number;
|
||||||
@@ -33,7 +32,7 @@ export const CanvasChart = ({ total, hueOffset = 0, domain, hardDomain, data, fo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!domain || historyMax > domain[1]) {
|
if (!domain || historyMax > domain[1]) {
|
||||||
return 1.25 * historyMax;
|
return historyMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
return domain[1];
|
return domain[1];
|
||||||
@@ -129,7 +128,7 @@ export const CanvasChart = ({ total, hueOffset = 0, domain, hardDomain, data, fo
|
|||||||
|
|
||||||
drawSeries('fill');
|
drawSeries('fill');
|
||||||
drawSeries('stroke');
|
drawSeries('stroke');
|
||||||
}, fps);
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='chart'>
|
<div className='chart'>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { siAtom } from '@/atoms';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { ChartCard } from './common/card';
|
import { ChartCard } from './common/card';
|
||||||
|
|
||||||
export const Disks = () => {
|
export const Disks = () => {
|
||||||
const { data: dynamicData } = useQuery<DynamicData>({ queryKey: ['dynamic'] });
|
const { data: dynamicData } = useQuery<DynamicData>({ queryKey: ['dynamic'] });
|
||||||
|
const isSi = useAtomValue(siAtom);
|
||||||
|
|
||||||
if (!dynamicData) {
|
if (!dynamicData) {
|
||||||
return <div />;
|
return <div />;
|
||||||
@@ -15,7 +18,7 @@ export const Disks = () => {
|
|||||||
labels: ['Read', 'Write'],
|
labels: ['Read', 'Write'],
|
||||||
}}
|
}}
|
||||||
hueOffset={120}
|
hueOffset={120}
|
||||||
formatOptions={{ units: 'B/s' }}
|
formatOptions={{ units: 'B/s', ...(isSi && { si: true }) }}
|
||||||
data={[dynamicData.disks.read, dynamicData.disks.write]}
|
data={[dynamicData.disks.read, dynamicData.disks.write]}
|
||||||
total={2}
|
total={2}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
|
import { siAtom } from '@/atoms';
|
||||||
import { formatValue } from '@/utils/format';
|
import { formatValue } from '@/utils/format';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { ChartCard } from './common/card';
|
import { ChartCard } from './common/card';
|
||||||
|
|
||||||
const formatOptions = { units: 'B' };
|
|
||||||
|
|
||||||
export const Memory = () => {
|
export const Memory = () => {
|
||||||
const { data: staticData } = useQuery<StaticData>({ queryKey: ['static'] });
|
const { data: staticData } = useQuery<StaticData>({ queryKey: ['static'] });
|
||||||
const { data: dynamicData } = useQuery<DynamicData>({ queryKey: ['dynamic'] });
|
const { data: dynamicData } = useQuery<DynamicData>({ queryKey: ['dynamic'] });
|
||||||
|
const isSi = useAtomValue(siAtom);
|
||||||
|
const formatOptions = { units: 'B', ...(isSi && { si: true }) };
|
||||||
|
|
||||||
const formatedTotals = useMemo(() => {
|
const formatedTotals = useMemo(() => {
|
||||||
if (!staticData) {
|
if (!staticData) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [formatValue(staticData.total_memory, formatOptions), formatValue(staticData.total_swap, formatOptions)];
|
return [formatValue(staticData.total_memory, formatOptions), formatValue(staticData.total_swap, formatOptions)];
|
||||||
}, [staticData]);
|
}, [staticData, formatOptions]);
|
||||||
|
|
||||||
if (!staticData || !dynamicData) {
|
if (!staticData || !dynamicData) {
|
||||||
return <div />;
|
return <div />;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { siAtom } from '@/atoms';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { ChartCard } from './common/card';
|
import { ChartCard } from './common/card';
|
||||||
|
|
||||||
export const Network = () => {
|
export const Network = () => {
|
||||||
const { data: dynamicData } = useQuery<DynamicData>({ queryKey: ['dynamic'] });
|
const { data: dynamicData } = useQuery<DynamicData>({ queryKey: ['dynamic'] });
|
||||||
|
const isSi = useAtomValue(siAtom);
|
||||||
|
|
||||||
if (!dynamicData) {
|
if (!dynamicData) {
|
||||||
return <div />;
|
return <div />;
|
||||||
@@ -15,7 +18,7 @@ export const Network = () => {
|
|||||||
labels: ['Down', 'Up'],
|
labels: ['Down', 'Up'],
|
||||||
}}
|
}}
|
||||||
hueOffset={60}
|
hueOffset={60}
|
||||||
formatOptions={{ units: 'B/s' }}
|
formatOptions={{ units: 'B/s', ...(isSi && { si: true }) }}
|
||||||
data={[dynamicData.network.down, dynamicData.network.up]}
|
data={[dynamicData.network.down, dynamicData.network.up]}
|
||||||
total={2}
|
total={2}
|
||||||
/>
|
/>
|
||||||
|
|||||||
7
src/client/components/chart-cards/static.css
Normal file
7
src/client/components/chart-cards/static.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.switches {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0px;
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { highFpsAtom, siAtom } from '@/atoms';
|
||||||
|
import { Switch } from '@/components/switch';
|
||||||
import { useAnimationFrame } from '@/hooks/use-animation-frame';
|
import { useAnimationFrame } from '@/hooks/use-animation-frame';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Switch } from '../switch';
|
import './static.css';
|
||||||
|
|
||||||
const formatUptime = (value: number) => {
|
const formatUptime = (value: number) => {
|
||||||
const seconds = String(Math.floor(value % 60)).padStart(2, '0');
|
const seconds = String(Math.floor(value % 60)).padStart(2, '0');
|
||||||
@@ -40,6 +43,8 @@ export const Static = () => {
|
|||||||
const { data: staticData } = useQuery<StaticData>({ queryKey: ['static'] });
|
const { data: staticData } = useQuery<StaticData>({ queryKey: ['static'] });
|
||||||
const root = useRef(document.getElementById('root')!);
|
const root = useRef(document.getElementById('root')!);
|
||||||
const [dark, setDark] = useState(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
const [dark, setDark] = useState(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
const [highFps, setHighFps] = useAtom(highFpsAtom);
|
||||||
|
const [isSi, setIsSi] = useAtom(siAtom);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
root.current.setAttribute('data-theme', dark ? 'dark' : 'light');
|
root.current.setAttribute('data-theme', dark ? 'dark' : 'light');
|
||||||
@@ -59,11 +64,25 @@ export const Static = () => {
|
|||||||
<h3>{staticData.kernel_version}</h3>
|
<h3>{staticData.kernel_version}</h3>
|
||||||
{staticData.boot_time && <Uptime boot_time={staticData.boot_time} />}
|
{staticData.boot_time && <Uptime boot_time={staticData.boot_time} />}
|
||||||
|
|
||||||
|
<div className='switches'>
|
||||||
<Switch
|
<Switch
|
||||||
checked={dark}
|
checked={dark}
|
||||||
label={`${dark ? 'Dark' : 'Light'} theme`}
|
label={`${dark ? 'Dark' : 'Light'} theme`}
|
||||||
onChange={({ target }) => setDark(target.checked)}
|
onChange={({ target }) => setDark(target.checked)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
checked={highFps}
|
||||||
|
label={`${highFps ? 'High' : 'Low'} FPS`}
|
||||||
|
onChange={({ target }) => setHighFps(target.checked)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
checked={isSi}
|
||||||
|
label={`Powers of ${isSi ? 10 : 2}`}
|
||||||
|
onChange={({ target }) => setIsSi(target.checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import { highFpsAtom } from '@/atoms';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
export const useAnimationFrame = (callback: (dt: number) => void, fps = 60) => {
|
export const useAnimationFrame = (callback: (dt: number) => void, fps?: number) => {
|
||||||
const ignored = useRef(0);
|
const ignored = useRef(0);
|
||||||
const requestRef = useRef<number>();
|
const requestRef = useRef<number>();
|
||||||
const previousTimeRef = useRef<number>();
|
const previousTimeRef = useRef<number>();
|
||||||
|
const highFps = useAtomValue(highFpsAtom);
|
||||||
|
const autoFps = fps ?? (highFps ? 30 : 4);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const animate: FrameRequestCallback = time => {
|
const animate: FrameRequestCallback = time => {
|
||||||
@@ -11,7 +15,7 @@ export const useAnimationFrame = (callback: (dt: number) => void, fps = 60) => {
|
|||||||
const deltaTime = time - previousTimeRef.current;
|
const deltaTime = time - previousTimeRef.current;
|
||||||
ignored.current += deltaTime;
|
ignored.current += deltaTime;
|
||||||
|
|
||||||
if (ignored.current > 1000 / fps) {
|
if (ignored.current > 1000 / autoFps) {
|
||||||
ignored.current = 0;
|
ignored.current = 0;
|
||||||
callback(deltaTime);
|
callback(deltaTime);
|
||||||
}
|
}
|
||||||
@@ -26,5 +30,5 @@ export const useAnimationFrame = (callback: (dt: number) => void, fps = 60) => {
|
|||||||
cancelAnimationFrame(requestRef.current);
|
cancelAnimationFrame(requestRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [callback, fps]);
|
}, [callback, autoFps]);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user