mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 13:23:28 +00:00
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:
parent
5e592682a9
commit
cc40d39d6f
|
@ -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
|
||||
|
|
|
@ -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]" />
|
||||
);
|
||||
|
|
|
@ -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 <></>;
|
||||
|
|
|
@ -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`
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
}}
|
||||
|
|
|
@ -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={[
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue