StatItems count up when amount changes (#226)

* simple skeleton system

* fix overview counter!

* nitpick: call `appPropsContext` value `appProps`

* fix contextMenu render key warning

Co-authored-by: Polar <polar@polar.blue>
Co-authored-by: maxichrome <maxichrome@users.noreply.github.com>
This commit is contained in:
maxichrome 2022-06-06 07:37:18 -05:00 committed by GitHub
parent 5e592682a9
commit cc40d39d6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 133 additions and 100 deletions

View file

@ -61,11 +61,10 @@ export interface AppProps {
}
function AppLayout() {
const appPropsContext = useContext(AppPropsContext);
const appProps = useContext(AppPropsContext);
const isWindowRounded = appPropsContext?.platform === 'macOS';
const hasWindowBorder =
appPropsContext?.platform !== 'browser' && appPropsContext?.platform !== 'windows';
const isWindowRounded = appProps?.platform === 'macOS';
const hasWindowBorder = appProps?.platform !== 'browser' && appProps?.platform !== 'windows';
return (
<div

View file

@ -27,6 +27,7 @@ export default function FileItem(props: Props) {
// />
// );
// };
return (
<WithContextMenu
menu={[
@ -115,11 +116,9 @@ export default function FileItem(props: Props) {
<path d="M41.4116 40.5577H11.234C5.02962 40.5577 0 35.5281 0 29.3238V0L41.4116 40.5577Z" />
</svg>
<div className="absolute flex flex-col items-center justify-center w-full h-full">
{/* @ts-ignore */}
{props.iconName && icons[props.iconName] ? (
{props.iconName && icons[props.iconName as keyof typeof icons] ? (
(() => {
// @ts-ignore
let Icon = icons[props.iconName];
const Icon = icons[props.iconName as keyof typeof icons];
return (
<Icon className="mt-2 pointer-events-none margin-auto w-[40px] h-[40px]" />
);

View file

@ -219,7 +219,7 @@ const RenderCell: React.FC<{
if (!file || !colKey || !dirId) return <></>;
const row = file;
if (!row) return <></>;
const appPropsContext = useContext(AppPropsContext);
const appProps = useContext(AppPropsContext);
const value = row[colKey];
if (!value) return <></>;

View file

@ -13,7 +13,7 @@ export default function FileThumb(props: {
hasThumbnailOverride: boolean;
className?: string;
}) {
const appPropsContext = useContext(AppPropsContext);
const appProps = useContext(AppPropsContext);
const { data: client } = useBridgeQuery('NodeGetState');
if (props.file.is_dir) {
@ -24,7 +24,7 @@ export default function FileThumb(props: {
return (
<img
className="pointer-events-none z-90"
src={appPropsContext?.convertFileSrc(
src={appProps?.convertFileSrc(
`${client.data_path}/thumbnails/${props.locationId}/${props.file.file?.cas_id}.webp`
)}
/>

View file

@ -55,14 +55,14 @@ export const MacWindowControlsSpace: React.FC<{
};
export function MacWindowControls() {
const appPropsContext = useContext(AppPropsContext);
const appProps = useContext(AppPropsContext);
return (
<MacWindowControlsSpace>
<MacTrafficLights
onClose={appPropsContext?.onClose}
onFullscreen={appPropsContext?.onFullscreen}
onMinimize={appPropsContext?.onMinimize}
onClose={appProps?.onClose}
onFullscreen={appProps?.onFullscreen}
onMinimize={appProps?.onMinimize}
className="z-50 absolute top-[13px] left-[13px]"
/>
</MacWindowControlsSpace>
@ -72,7 +72,7 @@ export function MacWindowControls() {
export const Sidebar: React.FC<SidebarProps> = (props) => {
const { isExperimental } = useNodeStore();
const appPropsContext = useContext(AppPropsContext);
const appProps = useContext(AppPropsContext);
const { data: locations } = useBridgeQuery('SysGetLocations');
const { data: clientState } = useBridgeQuery('NodeGetState');
@ -88,11 +88,10 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
return (
<div className="flex flex-col flex-grow-0 flex-shrink-0 w-48 min-h-full px-2.5 overflow-x-hidden overflow-y-scroll border-r border-gray-100 no-scrollbar bg-gray-50 dark:bg-gray-850 dark:border-gray-600">
{appPropsContext?.platform === 'browser' &&
window.location.search.includes('showControls') ? (
{appProps?.platform === 'browser' && window.location.search.includes('showControls') ? (
<MacWindowControls />
) : null}
{appPropsContext?.platform === 'macOS' ? <MacWindowControlsSpace /> : null}
{appProps?.platform === 'macOS' ? <MacWindowControlsSpace /> : null}
<Dropdown
buttonProps={{
@ -185,7 +184,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<button
onClick={() => {
appPropsContext?.openDialog({ directory: true }).then((result) => {
appProps?.openDialog({ directory: true }).then((result) => {
createLocation({ path: result });
});
}}

View file

@ -4,8 +4,10 @@ import { Statistics } from '@sd/core';
import { Button, Input } from '@sd/ui';
import byteSize from 'byte-size';
import clsx from 'clsx';
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect } from 'react';
import { useCountUp } from 'react-countup';
import Skeleton from 'react-loading-skeleton';
import 'react-loading-skeleton/dist/skeleton.css';
import create from 'zustand';
import { AppPropsContext } from '../App';
@ -13,90 +15,136 @@ import { Device } from '../components/device/Device';
import Dialog from '../components/layout/Dialog';
interface StatItemProps {
name: string;
value?: string;
unit?: string;
title: string;
bytes: string;
isLoading: boolean;
}
const StatItemNames: Partial<Record<keyof Statistics, string>> = {
total_bytes_capacity: 'Total capacity',
preview_media_bytes: 'Preview media',
library_db_size: 'Index size',
total_bytes_free: 'Free space'
};
type OverviewStats = Partial<Record<keyof Statistics, string>>;
type OverviewState = {
hasOverviewStatsRan: boolean;
setOverviewStatsRan: (ran: boolean) => void;
overviewStats: OverviewStats;
setOverviewStat: (name: keyof OverviewStats, newValue: string) => void;
setOverviewStats: (stats: OverviewStats) => void;
};
export const useOverviewState = create<OverviewState>((set) => ({
hasOverviewStatsRan: false,
setOverviewStatsRan: (ran: boolean) =>
overviewStats: {},
setOverviewStat: (name, newValue) =>
set((state) => ({
...state,
hasOverviewStatsRan: ran
overviewStats: {
...state.overviewStats,
[name]: newValue
}
})),
setOverviewStats: (stats) =>
set((state) => ({
...state,
overviewStats: stats
}))
}));
const StatItem: React.FC<StatItemProps> = (props) => {
const countUp = React.useRef(null);
const hiddenCountUp = React.useRef(null);
const appPropsContext = useContext(AppPropsContext);
let size = byteSize(Number(props.value) || 0);
const { title, bytes = '0', isLoading } = props;
let amount = parseFloat(size.value);
const appProps = useContext(AppPropsContext);
const { hasOverviewStatsRan, setOverviewStatsRan } = useOverviewState();
const size = byteSize(+bytes);
const counterRef = React.useRef<HTMLElement>(null);
const { update } = useCountUp({
ref: hasOverviewStatsRan ? hiddenCountUp : countUp,
end: amount,
const counter = useCountUp({
end: +size.value,
ref: counterRef,
delay: 0.1,
decimals: 1,
duration: appPropsContext?.demoMode ? 1 : 0.5,
useEasing: true,
onEnd: () => {
setOverviewStatsRan(true);
}
duration: appProps?.demoMode ? 1 : 0.5,
useEasing: true
});
useEffect(() => update(amount), [amount]);
useEffect(() => {
counter.update(+size.value);
}, [bytes]);
return (
<div
className={clsx(
'flex flex-col flex-shrink-0 w-32 px-4 py-3 duration-75 transform rounded-md cursor-default hover:bg-gray-50 hover:dark:bg-gray-600',
!amount && 'hidden'
!+bytes && 'hidden'
)}
>
<span className="text-sm text-gray-400">{props.name}</span>
<span className="text-sm text-gray-400">{title}</span>
<span className="text-2xl font-bold">
<span className="hidden" ref={hiddenCountUp} />
{hasOverviewStatsRan ? <span>{size.value}</span> : <span ref={countUp} />}
<span className="ml-1 text-[16px] text-gray-400">{size.unit}</span>
{/* <span className="hidden" aria-hidden="true" ref={hiddenCountUp} /> */}
{!isLoading ? (
<div>
<Skeleton enableAnimation={true} baseColor={'#21212e'} highlightColor={'#13131a'} />
</div>
) : (
<span className="tabular-nums" ref={counterRef}></span>
)}
{!isLoading ? <></> : <span className="ml-1 text-[16px] text-gray-400">{size.unit}</span>}
</span>
</div>
);
};
export const OverviewScreen: React.FC<{}> = (props) => {
const { data: libraryStatistics } = useBridgeQuery('GetLibraryStatistics');
const { data: clientState } = useBridgeQuery('NodeGetState');
export const OverviewScreen = () => {
const { data: libraryStatistics, isLoading: isStatisticsLoading } =
useBridgeQuery('GetLibraryStatistics');
const { data: nodeState } = useBridgeQuery('NodeGetState');
const [stats, setStats] = useState<Statistics>(libraryStatistics || ({} as Statistics));
const { overviewStats, setOverviewStats, setOverviewStat } = useOverviewState();
// get app props context
const appPropsContext = useContext(AppPropsContext);
// get app props from context
const appProps = useContext(AppPropsContext);
useEffect(() => {
if (appPropsContext?.demoMode == true && !libraryStatistics?.library_db_size) {
setStats({
total_bytes_capacity: '8093333345230',
preview_media_bytes: '2304387532',
library_db_size: '83345230',
total_file_count: 20342345,
total_bytes_free: '89734502034',
total_bytes_used: '8093333345230',
total_unique_bytes: '9347397'
});
if (appProps?.demoMode === true) {
if (!Object.entries(overviewStats).length)
setOverviewStats({
total_bytes_capacity: '8093333345230',
preview_media_bytes: '2304387532',
library_db_size: '83345230',
total_file_count: '20342345',
total_bytes_free: '89734502034',
total_bytes_used: '8093333345230',
total_unique_bytes: '9347397'
});
} else {
setStats(libraryStatistics as Statistics);
const newStatistics: OverviewStats = {
total_bytes_capacity: '0',
preview_media_bytes: '0',
library_db_size: '0',
total_file_count: '0',
total_bytes_free: '0',
total_bytes_used: '0',
total_unique_bytes: '0'
};
Object.entries(libraryStatistics as Statistics).forEach(([key, value]) => {
newStatistics[key as keyof Statistics] = `${value}`;
});
setOverviewStats(newStatistics);
}
}, [appPropsContext, libraryStatistics]);
}, [appProps, libraryStatistics]);
useEffect(() => {
setTimeout(() => {
setOverviewStat('total_bytes_capacity', '4093333345230');
}, 2000);
}, [overviewStats]);
// forgive me, father, for i have sinned with the typescript... except this literally makes sense
const displayableStatItems = Object.keys(StatItemNames) as unknown as keyof typeof StatItemNames;
return (
<div className="flex flex-col w-full h-screen overflow-x-hidden custom-scroll page-scroll">
@ -107,33 +155,18 @@ export const OverviewScreen: React.FC<{}> = (props) => {
<div className="flex w-full">
{/* STAT CONTAINER */}
<div className="flex pb-4 overflow-hidden">
<StatItem
name="Total capacity"
value={stats?.total_bytes_capacity}
unit={stats?.total_bytes_capacity}
/>
<StatItem
name="Index size"
value={stats?.library_db_size}
unit={stats?.library_db_size}
/>
<StatItem
name="Preview media"
value={stats?.preview_media_bytes}
unit={stats?.preview_media_bytes}
/>
<StatItem
name="Free space"
value={stats?.total_bytes_free}
unit={stats?.total_bytes_free}
/>
<StatItem name="Total at-risk" value={'0'} unit={stats?.preview_media_bytes} />
{/* <StatItem
name="Total at-risk"
value={'0'}
unit={stats?.preview_media_bytes}
/>
<StatItem name="Total backed up" value={'0'} unit={''} /> */}
{Object.entries(overviewStats).map(([key, value]) => {
if (!displayableStatItems.includes(key)) return null;
return (
<StatItem
key={key}
title={StatItemNames[key as keyof Statistics]!}
bytes={value}
isLoading={isStatisticsLoading}
/>
);
})}
</div>
<div className="flex-grow" />
<div className="space-x-2">
@ -170,9 +203,9 @@ export const OverviewScreen: React.FC<{}> = (props) => {
</div>
</div>
<div className="flex flex-col pb-4 space-y-4">
{clientState && (
{nodeState && (
<Device
name={clientState?.node_name ?? 'This Device'}
name={nodeState?.node_name ?? 'This Device'}
size="1.4TB"
runningJob={{ amount: 65, task: 'Generating preview media' }}
locations={[

View file

@ -33,16 +33,19 @@ export const ContextMenu: React.FC<ContextMenuProps> = (props) => {
{...rest}
>
{sections.map((sec, i) => (
<>
<React.Fragment key={i}>
{i !== 0 && (
<ContextMenuPrimitive.Separator className="border-0 border-b border-b-gray-300 dark:border-b-gray-550 mx-2" />
)}
<ContextMenuPrimitive.Group key={i} className="flex items-stretch flex-col gap-0.5">
<ContextMenuPrimitive.Group className="flex items-stretch flex-col gap-0.5">
{sec.map((item) => {
if (typeof item === 'string')
return (
<ContextMenuPrimitive.Label className="text-xs ml-2 mt-1 uppercase text-gray-400">
<ContextMenuPrimitive.Label
key={item}
className="text-xs ml-2 mt-1 uppercase text-gray-400"
>
{item}
</ContextMenuPrimitive.Label>
);
@ -94,7 +97,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = (props) => {
);
})}
</ContextMenuPrimitive.Group>
</>
</React.Fragment>
))}
</ContextMenuPrimitive.Content>
);