From f7b0e3bd06add6dbd8003b490f866862d2b57868 Mon Sep 17 00:00:00 2001 From: Matthew Yung <117509016+myung03@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:00:51 -0700 Subject: [PATCH] [ENG-1794] Overview Rework (#2555) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * StorageBar implementation * filekindstats reworked into interactive node graph * ui changes to graph * light/dark mode changes + ui edits * missing dependancy in package.json and minor visual improvements * fixed collision physics * d3 force package * fixed nodes going off screen * removed onNodeDragEnd * fix central node and add particle effect * fixed central node and typescript errors * bar graph/storage bar ui improvements * changed icons * totals * made ui changes * ui changes * minor requested ui changes * fixed spacing for ui * Remove extraneous newline in core/src/api/libraries.rs * Fix minor suggestions from code review * Fix typecheck * Auto format * refactor file kind stat card with css grid * ensure unidentified files never negative * FIxing some stats * Counting directories * fixed error with height of bars * updated storage bar * total files update --------- Co-authored-by: ameer2468 <33054370+ameer2468@users.noreply.github.com> Co-authored-by: James Pine Co-authored-by: Lynx <141365347+iLynxcat@users.noreply.github.com> Co-authored-by: VĂ­tor Vasconcellos Co-authored-by: Lynx Co-authored-by: Ericson Soares --- .../src/components/overview/Categories.tsx | 13 +- .../src/components/overview/CategoryItem.tsx | 26 +- .../src/screens/overview/Categories.tsx | 13 +- core/src/api/libraries.rs | 139 +++++- core/src/library/statistics.rs | 5 +- interface/app/$libraryId/network.tsx | 260 ++++++++++ .../app/$libraryId/overview/FileKindStats.tsx | 319 ++++++++---- .../app/$libraryId/overview/LibraryStats.tsx | 117 ++++- .../app/$libraryId/overview/StorageBar.tsx | 129 +++++ interface/app/$libraryId/overview/index.tsx | 76 +-- interface/locales/en/common.json | 3 + interface/package.json | 3 + packages/client/src/core.ts | 4 +- pnpm-lock.yaml | 462 +++++++++++++----- 14 files changed, 1217 insertions(+), 352 deletions(-) create mode 100644 interface/app/$libraryId/overview/StorageBar.tsx diff --git a/apps/mobile/src/components/overview/Categories.tsx b/apps/mobile/src/components/overview/Categories.tsx index a0fc2b754..e99791ac7 100644 --- a/apps/mobile/src/components/overview/Categories.tsx +++ b/apps/mobile/src/components/overview/Categories.tsx @@ -1,8 +1,8 @@ import { useNavigation } from '@react-navigation/native'; -import { useLibraryQuery } from '@sd/client'; import { DotsThree } from 'phosphor-react-native'; import React from 'react'; import { Text, View } from 'react-native'; +import { uint32ArrayToBigInt, useLibraryQuery } from '@sd/client'; import { tw } from '~/lib/tailwind'; import { OverviewStackScreenProps } from '~/navigation/tabs/OverviewStack'; @@ -24,12 +24,17 @@ export default function CategoriesScreen() { style={tw`h-8 w-8 rounded-full`} variant="gray" > - + {kinds.data?.statistics - ?.sort((a, b) => b.count - a.count) + ?.sort((a, b) => { + const aCount = uint32ArrayToBigInt(a.count); + const bCount = uint32ArrayToBigInt(b.count); + if (aCount === bCount) return 0; + return aCount > bCount ? -1 : 1; + }) .filter((i) => i.kind !== 0) .slice(0, 6) .map((item) => { @@ -49,7 +54,7 @@ export default function CategoriesScreen() { kind={kind} name={name} icon={icon} - items={count} + items={uint32ArrayToBigInt(count)} /> ); })} diff --git a/apps/mobile/src/components/overview/CategoryItem.tsx b/apps/mobile/src/components/overview/CategoryItem.tsx index 1dacbc048..8c4409fe6 100644 --- a/apps/mobile/src/components/overview/CategoryItem.tsx +++ b/apps/mobile/src/components/overview/CategoryItem.tsx @@ -1,16 +1,16 @@ -import { formatNumber } from '@sd/client'; +import { useNavigation } from '@react-navigation/native'; import { Pressable, Text, View } from 'react-native'; import { ClassInput } from 'twrnc'; +import { formatNumber } from '@sd/client'; import { tw, twStyle } from '~/lib/tailwind'; - -import { useNavigation } from '@react-navigation/native'; import { useSearchStore } from '~/stores/searchStore'; + import { Icon, IconName } from '../icons/Icon'; interface CategoryItemProps { kind: number; name: string; - items: number; + items: bigint | number; icon: IconName; selected?: boolean; onClick?: () => void; @@ -29,14 +29,18 @@ const CategoryItem = ({ name, icon, items, style, kind }: CategoryItemProps) => style )} onPress={() => { - searchStore.updateFilters('kind', { - name, - icon: icon + '20' as IconName, - id: kind - }, true); + searchStore.updateFilters( + 'kind', + { + name, + icon: (icon + '20') as IconName, + id: kind + }, + true + ); navigation.navigate('SearchStack', { - screen: 'Search', - }) + screen: 'Search' + }); }} > diff --git a/apps/mobile/src/screens/overview/Categories.tsx b/apps/mobile/src/screens/overview/Categories.tsx index 65a4317a0..812229f0e 100644 --- a/apps/mobile/src/screens/overview/Categories.tsx +++ b/apps/mobile/src/screens/overview/Categories.tsx @@ -1,7 +1,7 @@ -import { useLibraryQuery } from '@sd/client'; import { useMemo } from 'react'; import { FlatList, View } from 'react-native'; import { useDebounce } from 'use-debounce'; +import { uint32ArrayToBigInt, useLibraryQuery } from '@sd/client'; import { IconName } from '~/components/icons/Icon'; import ScreenContainer from '~/components/layout/ScreenContainer'; import CategoryItem from '~/components/overview/CategoryItem'; @@ -22,7 +22,14 @@ const CategoriesScreen = () => { return ( b.count - a.count).filter((i) => i.kind !== 0)} + data={filteredKinds + ?.sort((a, b) => { + const aCount = uint32ArrayToBigInt(a.count); + const bCount = uint32ArrayToBigInt(b.count); + if (aCount === bCount) return 0; + return aCount > bCount ? -1 : 1; + }) + .filter((i) => i.kind !== 0)} numColumns={3} contentContainerStyle={tw`py-6`} keyExtractor={(item) => item.name} @@ -46,7 +53,7 @@ const CategoriesScreen = () => { kind={kind} name={name} icon={icon} - items={count} + items={uint32ArrayToBigInt(count)} /> ); }} diff --git a/core/src/api/libraries.rs b/core/src/api/libraries.rs index 49d964cb0..26bf2506f 100644 --- a/core/src/api/libraries.rs +++ b/core/src/api/libraries.rs @@ -6,17 +6,15 @@ use crate::{ Node, }; -use futures::StreamExt; -use prisma_client_rust::raw; use sd_core_heavy_lifting::JobId; + use sd_file_ext::kind::ObjectKind; use sd_p2p::RemoteIdentity; -use sd_prisma::prisma::{indexer_rule, object, statistics}; -use tokio_stream::wrappers::IntervalStream; -use tracing::{info, warn}; +use sd_prisma::prisma::{file_path, indexer_rule, object, statistics}; +use sd_utils::{db::size_in_bytes_from_db, u64_to_frontend}; use std::{ - collections::{hash_map::Entry, HashMap}, + collections::{hash_map::Entry, BTreeMap, HashMap}, convert::identity, pin::pin, sync::Arc, @@ -25,8 +23,13 @@ use std::{ use async_channel as chan; use directories::UserDirs; -use futures_concurrency::{future::Join, stream::Merge}; +use futures::StreamExt; +use futures_concurrency::{ + future::{Join, TryJoin}, + stream::Merge, +}; use once_cell::sync::Lazy; +use prisma_client_rust::{and, or, raw}; use rspc::{alpha::AlphaRouter, ErrorCode}; use serde::{Deserialize, Serialize}; use specta::Type; @@ -36,7 +39,8 @@ use tokio::{ sync::Mutex, time::{interval, Instant}, }; -use tracing::{debug, error}; +use tokio_stream::wrappers::IntervalStream; +use tracing::{debug, error, info, warn}; use uuid::Uuid; use super::{utils::library, Ctx, R}; @@ -122,36 +126,125 @@ pub(crate) fn mount() -> AlphaRouter { }) }) .procedure("kindStatistics", { - #[derive(Serialize, Deserialize, Type, Default)] + #[derive(Debug, Serialize, Deserialize, Type, Default)] pub struct KindStatistic { kind: i32, name: String, - count: i32, - total_bytes: String, + count: (u32, u32), + total_bytes: (u32, u32), } - #[derive(Serialize, Deserialize, Type, Default)] + #[derive(Debug, Serialize, Deserialize, Type, Default)] pub struct KindStatistics { statistics: Vec, + total_identified_files: i32, + total_unidentified_files: i32, } + + #[derive(Default)] + struct CountAndSize { + count: u64, + size: u64, + } + R.with2(library()).query(|(_, library), _: ()| async move { - let mut statistics: Vec = vec![]; - for kind in ObjectKind::iter() { - let count = library + let (total_unidentified_files, total_identified_files) = ( + library + .db + .file_path() + .count(vec![ + file_path::is_dir::equals(Some(false)), + file_path::cas_id::equals(None), + file_path::object_id::equals(None), + ]) + .exec(), + library + .db + .file_path() + .count(vec![or!( + file_path::is_dir::equals(Some(true)), + and!( + file_path::cas_id::not(None), + file_path::object_id::not(None), + ), + )]) + .exec(), + ) + .try_join() + .await?; + + let mut statistics_by_kind = BTreeMap::from_iter( + ObjectKind::iter().map(|kind| (kind as i32, CountAndSize::default())), + ); + + let mut last_object_id = 0; + + loop { + let objects = library .db .object() - .count(vec![object::kind::equals(Some(kind as i32))]) + .find_many(vec![object::id::gt(last_object_id)]) + .take(1000) + .select( + object::select!({ id kind file_paths: select { size_in_bytes_bytes } }), + ) .exec() .await?; - statistics.push(KindStatistic { - kind: kind as i32, - name: kind.to_string(), - count: count as i32, - total_bytes: "0".to_string(), - }); + if let Some(last) = objects.last() { + last_object_id = last.id; + } else { + break; // No more objects + } + + for object in objects { + if let Some(kind) = object.kind { + statistics_by_kind.entry(kind).and_modify(|count_and_size| { + count_and_size.count += object.file_paths.len() as u64; + count_and_size.size += object + .file_paths + .into_iter() + .map(|file_path| { + file_path + .size_in_bytes_bytes + .map(|size| size_in_bytes_from_db(&size)) + .unwrap_or(0) + }) + .sum::(); + }); + } + } } - Ok(KindStatistics { statistics }) + // This is a workaround for the fact that we don't assign object to directories yet + if let Some(count_and_size) = + statistics_by_kind.get_mut(&(ObjectKind::Folder as i32)) + { + count_and_size.count = library + .db + .file_path() + .count(vec![file_path::is_dir::equals(Some(true))]) + .exec() + .await? as u64; + } + + Ok(KindStatistics { + statistics: ObjectKind::iter() + .map(|kind| { + let int_kind = kind as i32; + let CountAndSize { count, size } = + statistics_by_kind.get(&int_kind).expect("can't fail"); + + KindStatistic { + kind: int_kind, + name: kind.to_string(), + count: u64_to_frontend(*count), + total_bytes: u64_to_frontend(*size), + } + }) + .collect(), + total_identified_files: total_identified_files as i32, + total_unidentified_files: total_unidentified_files as i32, + }) }) }) .procedure("create", { diff --git a/core/src/library/statistics.rs b/core/src/library/statistics.rs index 74c7fcd87..7746975fb 100644 --- a/core/src/library/statistics.rs +++ b/core/src/library/statistics.rs @@ -1,11 +1,12 @@ use crate::{api::utils::get_size, library::Library, volume::get_volumes, Node}; use sd_prisma::prisma::statistics; +use sd_utils::db::size_in_bytes_from_db; use chrono::Utc; +use tracing::{error, info}; use super::LibraryManagerError; -use tracing::{error, info}; pub async fn update_library_statistics( node: &Node, @@ -45,7 +46,7 @@ pub async fn update_library_statistics( .map(|location| { location .size_in_bytes - .map(|bytes| bytes.iter().fold(0, |acc, &x| acc * 256 + x as u64)) + .map(|size| size_in_bytes_from_db(&size)) .unwrap_or(0) }) .sum::(); diff --git a/interface/app/$libraryId/network.tsx b/interface/app/$libraryId/network.tsx index 93b68d58b..5d1fffa0b 100644 --- a/interface/app/$libraryId/network.tsx +++ b/interface/app/$libraryId/network.tsx @@ -72,3 +72,263 @@ export const Component = () => { ); }; + +// NOTE -> this is code for the node graph. The plan is to implement this in network (moved from overview page). Jamie asked me to save the code somewhere +// so placing it here for now! + +// import { getIcon } from '@sd/assets/util'; +// import { useLibraryQuery } from '@sd/client'; +// import React, { useEffect, useRef, useState, useCallback } from 'react'; +// import { useIsDark } from '~/hooks'; +// import ForceGraph2D from 'react-force-graph-2d'; +// import { useNavigate } from 'react-router'; +// import * as d3 from 'd3-force'; + +// //million-ignore +// const canvasWidth = 700 +// const canvasHeight = 600; + +// interface KindStatistic { +// kind: number; +// name: string; +// count: number; +// total_bytes: string; +// } + +// interface Node { +// id: string | number; +// name: string; +// val: number; +// fx?: number; +// fy?: number; +// x?: number; +// y?: number; +// } + +// interface Link { +// source: string | number; +// target: string | number; +// } + +// interface GraphData { +// nodes: Node[]; +// links: Link[]; +// } + +// const FileKindStatistics: React.FC = () => { +// const isDark = useIsDark(); +// const navigate = useNavigate(); +// const { data } = useLibraryQuery(['library.kindStatistics']); +// const [graphData, setGraphData] = useState({ nodes: [], links: [] }); +// const iconsRef = useRef<{ [key: string]: HTMLImageElement }>({}); +// const containerRef = useRef(null); +// const fgRef = useRef(null); + +// useEffect(() => { +// if (data) { +// const statistics: KindStatistic[] = data.statistics +// .filter((item: KindStatistic) => item.count != 0) +// .sort((a: KindStatistic, b: KindStatistic) => b.count - a.count) +// // TODO: eventually allow users to select and save which file kinds are shown +// .slice(0, 18); // Get the top 18 highest file kinds + +// const totalFilesCount = statistics.reduce((sum, item) => sum + item.count, 0); +// const nodes = [ +// { id: 'center', name: 'Total Files', val: totalFilesCount }, +// ...statistics.map(item => ({ +// id: item.kind, +// name: item.name, +// val: item.count, +// })) +// ]; + +// const links = statistics.map(item => ({ +// source: 'center', +// target: item.kind, +// })); + +// setGraphData({ nodes, links }); + +// // Preload icons, this is for rendering purposes +// statistics.forEach(item => { +// const iconName = item.name; +// if (!iconsRef.current[iconName]) { +// const img = new Image(); +// img.src = getIcon(iconName, isDark); +// iconsRef.current[iconName] = img; +// } +// }); + +// // d3 stuff for changing physics of the nodes +// fgRef.current.d3Force('link').distance(110); // Adjust link distance to make links shorter +// fgRef.current.d3Force('charge').strength(-50); // how hard the nodes repel +// fgRef.current.d3Force('center').strength(10); // Adjust center strength for stability +// fgRef.current.d3Force('collision', d3.forceCollide().radius(25)); // Add collision force with radius. Should be a little larger than radius of nodes. + +// fgRef.current.d3Force('y', d3.forceY(canvasHeight / 5).strength((1.2))); // strong force to ensure nodes don't spill out of canvas +// } +// }, [data, isDark]); + +// const paintNode = useCallback((node: any, ctx: CanvasRenderingContext2D, globalScale: number) => { +// const fontSize = 0.6 / globalScale; +// ctx.font = `400 ${fontSize}em ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`; +// ctx.textAlign = 'center'; +// ctx.textBaseline = 'middle'; + +// const darkColor = 'rgb(34, 34, 45)'; +// const lightColor = 'rgb(252, 252, 254)'; + +// const x = isFinite(node.x) ? node.x : 0; +// const y = isFinite(node.y) ? node.y : 0; + +// if (node.name === 'Total Files') { +// const radius = 25; +// const borderWidth = 0.5; + +// // Create linear gradient for light mode +// const lightGradient = ctx.createLinearGradient(x - radius, y - radius, x + radius, y + radius); +// lightGradient.addColorStop(0, 'rgb(117, 177, 249)'); +// lightGradient.addColorStop(1, 'rgb(0, 76, 153)'); + +// // Create linear gradient for dark mode +// const darkGradient = ctx.createLinearGradient(x - radius, y - radius, x + radius, y + radius); +// darkGradient.addColorStop(0, 'rgb(255, 13, 202)'); +// darkGradient.addColorStop(1, 'rgb(128, 0, 255)'); + +// // Draw filled circle with gradient border +// ctx.beginPath(); +// ctx.arc(node.x, node.y, radius, 0, 2 * Math.PI, false); +// ctx.fillStyle = isDark ? darkGradient : lightGradient; +// ctx.fill(); + +// // Draw inner circle to create the border effect +// ctx.beginPath(); +// ctx.arc(node.x, node.y, radius - borderWidth, 0, 2 * Math.PI, false); +// ctx.fillStyle = isDark ? darkColor : lightColor; +// ctx.fill(); + +// // Add inner shadow +// const shadowGradient = ctx.createRadialGradient(x, y, radius * 0.5, x, y, radius); +// shadowGradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); +// shadowGradient.addColorStop(1, isDark ? 'rgba(255, 93, 234, 0.1' : 'rgba(66, 97, 255, 0.05)'); + +// ctx.globalCompositeOperation = 'source-atop'; +// ctx.beginPath(); +// ctx.arc(node.x, node.y, radius, 0, 2 * Math.PI, false); +// ctx.fillStyle = shadowGradient; +// ctx.fill(); + +// // Draw text +// ctx.fillStyle = isDark ? 'rgba(255, 255, 255, 1)' : 'rgba(10, 10, 10, 0.8)'; +// ctx.font = `bold ${fontSize * 2}em ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`; +// ctx.fillText(node.val, node.x, node.y - fontSize * 9); + +// ctx.fillStyle = isDark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(10, 10, 10, 0.8)'; +// ctx.font = `400 ${fontSize * 1.1}em ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`; +// ctx.fillText(node.name, node.x, node.y + fontSize * 25); +// } else { +// const iconName = node.name; +// const iconImg = iconsRef.current[iconName]; +// const iconSize = 25 / globalScale; +// const textYPos = node.y + iconSize; + +// // Draw shadow +// ctx.shadowColor = isDark ? 'rgb(44, 45, 58)' : 'rgba(0, 0, 0, 0.1)'; +// ctx.shadowBlur = 0.5; +// ctx.shadowOffsetX = -0.5; +// ctx.shadowOffsetY = -2; + +// // Draw node circle +// const radius = 18; +// ctx.beginPath(); +// ctx.arc(node.x, node.y, radius, 0, 2 * Math.PI, false); +// ctx.fillStyle = isDark ? darkColor : lightColor; +// ctx.fill(); +// ctx.shadowColor = 'transparent'; + +// if (iconImg) { +// ctx.drawImage(iconImg, node.x - iconSize / 2, node.y - iconSize, iconSize, iconSize); +// } + +// ctx.fillStyle = isDark ? 'white' : 'black'; + +// // Truncate node name if it is too long +// let truncatedName = node.name; +// if (node.name.length > 10) { +// truncatedName = node.name.slice(0, 6) + "..."; +// } +// ctx.fillText(truncatedName, node.x, textYPos - 9); + +// ctx.fillStyle = isDark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.5)'; +// ctx.fillText(node.val, node.x, textYPos - 2); +// } +// }, [isDark]); + +// const handleNodeClick = useCallback((node: any) => { +// if (node.id !== 'center') { +// const path = { +// pathname: '../search', +// search: new URLSearchParams({ +// filters: JSON.stringify([{ object: { kind: { in: [node.id] } } }]) +// }).toString() +// }; +// navigate(path); +// } +// }, [navigate]); + +// const handleEngineTick = () => { +// const centerNode = graphData.nodes.find((node: any) => node.id === 'center'); +// if (centerNode) { +// centerNode.fx = 0; +// centerNode.fy = 0; +// } +// } + +// useEffect(() => { +// if (fgRef.current) { +// fgRef.current.d3Force('center', d3.forceCenter()); +// } +// }, []); + +// const paintPointerArea = useCallback((node: any, color: string, ctx: CanvasRenderingContext2D, globalScale: number) => { +// const size = 30 / globalScale; +// ctx.fillStyle = color; +// ctx.beginPath(); +// ctx.arc(node.x, node.y, size, 0, 2 * Math.PI, false); +// ctx.fill(); +// }, []); + +// return ( +//
+// {data ? ( +// isDark ? '#2C2D3A' : 'rgba(0, 0, 0, 0.2)'} +// onNodeClick={handleNodeClick} +// enableZoomInteraction={false} +// enablePanInteraction={false} +// dagLevelDistance={100} +// warmupTicks={500} +// d3VelocityDecay={0.75} +// onEngineTick={handleEngineTick} +// nodePointerAreaPaint={paintPointerArea} +// /> +// ) : ( +//
Loading...
+// )} +//
+// ); +// }; + +// export default React.memo(FileKindStatistics); diff --git a/interface/app/$libraryId/overview/FileKindStats.tsx b/interface/app/$libraryId/overview/FileKindStats.tsx index 6f284ca8f..1249bb969 100644 --- a/interface/app/$libraryId/overview/FileKindStats.tsx +++ b/interface/app/$libraryId/overview/FileKindStats.tsx @@ -1,99 +1,252 @@ +import { Info } from '@phosphor-icons/react'; +import { getIcon } from '@sd/assets/util'; import clsx from 'clsx'; import { motion } from 'framer-motion'; -import { useRef } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { formatNumber, SearchFilterArgs, useLibraryQuery } from '@sd/client'; -import { Icon } from '~/components'; -import { useLocale } from '~/hooks'; +import React, { MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router'; +import { KindStatistic, uint32ArrayToBigInt, useLibraryQuery } from '@sd/client'; +import { Card, Tooltip } from '@sd/ui'; +import { useIsDark, useLocale } from '~/hooks'; -import { translateKindName } from '../Explorer/util'; +const INFO_ICON_CLASSLIST = 'inline size-3 text-ink-faint opacity-0'; +const TOTAL_FILES_CLASSLIST = + 'flex items-center justify-between whitespace-nowrap text-sm font-medium text-ink-dull mt-2 px-1'; +const UNIDENTIFIED_FILES_CLASSLIST = 'relative flex items-center text-xs text-ink-faint'; -export default () => { - const ref = useRef(null); - - const kinds = useLibraryQuery(['library.kindStatistics']); - - return ( - <> - {/* This is awful, will replace icons accordingly and memo etc */} - {kinds.data?.statistics - ?.sort((a, b) => b.count - a.count) - .filter((i) => i.kind !== 0) - .map(({ kind, name, count }) => { - let icon = name; - switch (name) { - case 'Code': - icon = 'Terminal'; - break; - case 'Unknown': - icon = 'Undefined'; - break; - } - return ( - - {}} - /> - - ); - })} - - ); +const mapFractionalValue = (numerator: bigint, denominator: bigint, maxValue: bigint): string => { + const result = ((numerator * maxValue) / denominator).toString(); + return result; }; -interface KindItemProps { - kind: number; - name: string; - items: number; - icon: string; - selected?: boolean; - onClick?: () => void; - disabled?: boolean; +const formatNumberWithCommas = (number: number | bigint) => number.toLocaleString(); + +const interpolateHexColor = (color1: string, color2: string, factor: number): string => { + const hex = (color: string) => parseInt(color.slice(1), 16); + const r = Math.round((1 - factor) * (hex(color1) >> 16) + factor * (hex(color2) >> 16)); + const g = Math.round( + (1 - factor) * ((hex(color1) >> 8) & 0x00ff) + factor * ((hex(color2) >> 8) & 0x00ff) + ); + const b = Math.round( + (1 - factor) * (hex(color1) & 0x0000ff) + factor * (hex(color2) & 0x0000ff) + ); + return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`; +}; + +interface FileKind { + kind: string; + count: bigint; + id: number; } -const KindItem = ({ kind, name, icon, items, selected, onClick, disabled }: KindItemProps) => { +interface FileKindStatsProps {} + +const FileKindStats: React.FC = () => { + const isDark = useIsDark(); + const navigate = useNavigate(); const { t } = useLocale(); - return ( - ([]); + const [cardWidth, setCardWidth] = useState(0); + const containerRef = useRef(null); + const iconsRef = useRef<{ [key: string]: HTMLImageElement }>({}); + + const BAR_MAX_HEIGHT = 115n; + const BAR_COLOR_START = '#3A7ECC'; + const BAR_COLOR_END = '#004C99'; + + const formatCount = (count: number | bigint): string => { + const bigIntCount = typeof count === 'number' ? BigInt(count) : count; + + return bigIntCount >= 1000n ? `${bigIntCount / 1000n}K` : count.toString(); + }; + + const handleResize = useCallback(() => { + if (containerRef.current) { + const factor = window.innerWidth > 1500 ? 0.35 : 0.4; + setCardWidth(window.innerWidth * factor); + } + }, []); + + useEffect(() => { + window.addEventListener('resize', handleResize); + handleResize(); + + const containerElement = containerRef.current; + if (containerElement) { + const observer = new MutationObserver(handleResize); + observer.observe(containerElement, { + attributes: true, + childList: true, + subtree: true, + attributeFilter: ['style'] + }); + + return () => { + observer.disconnect(); + }; + } + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [handleResize]); + + useEffect(() => { + if (data) { + const statistics: KindStatistic[] = data.statistics + .filter( + (item: { kind: number; count: any }) => uint32ArrayToBigInt(item.count) !== 0n + ) + .sort((a: { count: any }, b: { count: any }) => { + const aCount = uint32ArrayToBigInt(a.count); + const bCount = uint32ArrayToBigInt(b.count); + if (aCount === bCount) return 0; + return aCount > bCount ? -1 : 1; + }); + + setFileKinds( + statistics.map((item) => ({ + kind: item.name, + count: uint32ArrayToBigInt(item.count), + id: item.kind + })) + ); + + statistics.forEach((item) => { + const iconName = item.name; + if (!iconsRef.current[iconName]) { + const img = new Image(); + img.src = getIcon(iconName + '20', isDark); + iconsRef.current[iconName] = img; + } + }); + } + }, [data, isDark]); + + const sortedFileKinds = [...fileKinds].sort((a, b) => { + if (a.count === b.count) return 0; + return a.count > b.count ? -1 : 1; + }); + + const maxFileCount = sortedFileKinds && sortedFileKinds[0] ? sortedFileKinds[0].count : 0n; + + const barGap = 12; + const barCount = sortedFileKinds.length; + const totalGapWidth = barGap * (barCount - 5); + const makeBarClickHandler = + (fileKind: FileKind): MouseEventHandler | undefined => + () => { + const path = { pathname: '../search', search: new URLSearchParams({ - filters: JSON.stringify([ - { object: { kind: { in: [kind] } } } - ] as SearchFilterArgs[]) + filters: JSON.stringify([{ object: { kind: { in: [fileKind.id] } } }]) }).toString() - }} - > -
+ - -
-

{name}

- {items !== undefined && ( -

- {t('item_with_count', { count: items })} -

- )} +
+ +
+ + {data?.total_identified_files + ? formatNumberWithCommas(data.total_identified_files) + : '0'}{' '} + +
+ {t('total_files')} + +
+
+
+
+ + + {data?.total_unidentified_files + ? formatNumberWithCommas(data.total_unidentified_files) + : '0'}{' '} + unidentified files + + +
-
- +
+ {sortedFileKinds.map((fileKind, index) => { + const iconImage = iconsRef.current[fileKind.kind]; + const barColor = interpolateHexColor( + BAR_COLOR_START, + BAR_COLOR_END, + index / (barCount - 1) + ); + + const barHeight = + mapFractionalValue(fileKind.count, maxFileCount, BAR_MAX_HEIGHT) + 'px'; + return ( + <> + +
+ {iconImage && ( + {fileKind.kind} + )} + +
+
+
+ {formatCount(fileKind.count)} +
+ + ); + })} +
+
+
); }; + +export default FileKindStats; diff --git a/interface/app/$libraryId/overview/LibraryStats.tsx b/interface/app/$libraryId/overview/LibraryStats.tsx index b000f82f6..c04fcdb9d 100644 --- a/interface/app/$libraryId/overview/LibraryStats.tsx +++ b/interface/app/$libraryId/overview/LibraryStats.tsx @@ -2,8 +2,10 @@ import { Info } from '@phosphor-icons/react'; import clsx from 'clsx'; import { useEffect, useState } from 'react'; import { humanizeSize, Statistics, useLibraryContext, useLibraryQuery } from '@sd/client'; -import { Tooltip } from '@sd/ui'; -import { useCounter, useLocale } from '~/hooks'; +import { Card, Tooltip } from '@sd/ui'; +import { useCounter, useIsDark, useLocale } from '~/hooks'; + +import StorageBar from './StorageBar'; interface StatItemProps { title: string; @@ -12,15 +14,18 @@ interface StatItemProps { info?: string; } +interface Section { + name: string; + value: number; + color: string; + tooltip: string; +} + let mounted = false; const StatItem = (props: StatItemProps) => { const { title, bytes, isLoading } = props; - // This is important to the counter working. - // On first render of the counter this will potentially be `false` which means the counter should the count up. - // but in a `useEffect` `mounted` will be set to `true` so that subsequent renders of the counter will not run the count up. - // The acts as a cache of the value of `mounted` on the first render of this `StateItem`. const [isMounted] = useState(mounted); const size = humanizeSize(bytes); @@ -36,7 +41,7 @@ const StatItem = (props: StatItemProps) => { return (
@@ -46,7 +51,7 @@ const StatItem = (props: StatItemProps) => { )} @@ -69,42 +74,101 @@ const StatItem = (props: StatItemProps) => { }; const LibraryStats = () => { + const isDark = useIsDark(); const { library } = useLibraryContext(); - const stats = useLibraryQuery(['library.statistics']); + const storageBarData = useLibraryQuery(['library.kindStatistics']).data?.statistics; + console.log(storageBarData); + console.log(stats); + const { t } = useLocale(); useEffect(() => { if (!stats.isLoading) mounted = true; - }); - - const { t } = useLocale(); + }, [stats.isLoading]); const StatItemNames: Partial> = { + total_local_bytes_capacity: t('total_bytes_capacity'), + total_local_bytes_free: t('total_bytes_free'), total_library_bytes: t('library_bytes'), library_db_size: t('library_db_size'), - total_local_bytes_capacity: t('total_bytes_capacity'), - total_library_preview_media_bytes: t('preview_media_bytes'), - total_local_bytes_free: t('total_bytes_free'), - total_local_bytes_used: t('total_bytes_used') + total_library_preview_media_bytes: t('preview_media_bytes') }; const StatDescriptions: Partial> = { total_local_bytes_capacity: t('total_bytes_capacity_description'), - total_library_preview_media_bytes: t('preview_media_bytes_description'), + total_local_bytes_free: t('total_bytes_free_description'), total_library_bytes: t('library_bytes_description'), library_db_size: t('library_db_size_description'), - total_local_bytes_free: t('total_bytes_free_description'), - total_local_bytes_used: t('total_bytes_used_description') + total_library_preview_media_bytes: t('preview_media_bytes_description') }; const displayableStatItems = Object.keys( StatItemNames ) as unknown as keyof typeof StatItemNames; + + if (!stats.data || !stats.data.statistics) { + return
Loading...
; + } + + const { statistics } = stats.data; + const totalSpace = Number(statistics.total_local_bytes_capacity); + const totalUsedSpace = Number(statistics.total_local_bytes_used); + + // Define the major categories and aggregate the "Other" category + const majorCategories = ['Document', 'Text', 'Image', 'Video']; + const aggregatedData = (storageBarData ?? []).reduce( + (acc, curr) => { + const category = majorCategories.includes(curr.name) ? curr.name : 'Other'; + if (!acc[category]) { + acc[category] = { total_bytes: 0 }; + } + acc[category]!.total_bytes += curr.total_bytes[1]; + return acc; + }, + {} as Record + ); + + // Calculate the used space and determine the System Data + const usedSpace = Object.values(aggregatedData).reduce( + (acc, curr) => acc + curr.total_bytes, + 0 + ); + const systemDataBytes = totalUsedSpace - usedSpace; + + if (!aggregatedData['Other']) { + aggregatedData['Other'] = { total_bytes: 0 }; + } + + const sections: Section[] = Object.entries(aggregatedData).map(([name, data], index) => { + const colors = [ + '#3A7ECC', // Slightly Darker Blue 400 + '#AAAAAA', // Gray + '#004C99', // Tailwind Blue 700 + '#2563EB', // Tailwind Blue 500 + '#00274D' // Dark Navy Blue, + ]; + + const color = colors[index % colors.length] || '#8F8F8F'; // Use a default color if colors array is empty + return { + name, + value: data.total_bytes, + color, + tooltip: `${name}` + }; + }); + + // Add System Data section + sections.push({ + name: 'System Data', + value: systemDataBytes, + color: '#707070', // Gray for System Data + tooltip: 'System data that exists outside of your Spacedrive library' + }); + return ( -
-
- {Object.entries(stats?.data?.statistics || []) - // sort the stats by the order of the displayableStatItems + +
+ {Object.entries(statistics) .sort( ([a], [b]) => displayableStatItems.indexOf(a) - displayableStatItems.indexOf(b) @@ -115,14 +179,17 @@ const LibraryStats = () => { ); })}
-
+
+ +
+ ); }; diff --git a/interface/app/$libraryId/overview/StorageBar.tsx b/interface/app/$libraryId/overview/StorageBar.tsx new file mode 100644 index 000000000..ba5e21e7c --- /dev/null +++ b/interface/app/$libraryId/overview/StorageBar.tsx @@ -0,0 +1,129 @@ +import React, { useState } from 'react'; +import { humanizeSize } from '@sd/client'; +import { Tooltip } from '@sd/ui'; +import { useIsDark } from '~/hooks'; + +const BARWIDTH = 700; + +const lightenColor = (color: string, percent: number) => { + const num = parseInt(color.replace('#', ''), 16); + const amt = Math.round(2.55 * percent); + const R = (num >> 16) + amt; + const G = ((num >> 8) & 0x00ff) + amt; + const B = (num & 0x0000ff) + amt; + return `#${( + 0x1000000 + + (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + + (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 + + (B < 255 ? (B < 1 ? 0 : B) : 255) + ) + .toString(16) + .slice(1) + .toUpperCase()}`; +}; + +interface Section { + name: string; + value: number; + color: string; + tooltip: string; +} + +interface StorageBarProps { + sections: Section[]; + totalSpace: number; +} + +const StorageBar: React.FC = ({ sections, totalSpace }) => { + const isDark = useIsDark(); + const [hoveredSectionIndex, setHoveredSectionIndex] = useState(null); + + const getPercentage = (value: number) => { + const percentage = value / totalSpace; + const pixvalue = BARWIDTH * percentage; + return `${pixvalue.toFixed(2)}px`; + }; + + const usedSpace = sections.reduce((acc, section) => acc + section.value, 0); + const unusedSpace = totalSpace - usedSpace; + + const nonSystemSections = sections.filter((section) => section.name !== 'System Data'); + const systemSection = sections.find((section) => section.name === 'System Data'); + + return ( +
+
+ {nonSystemSections.map((section, index) => { + const humanizedValue = humanizeSize(section.value); + const isHovered = hoveredSectionIndex === index; + + return ( + +
setHoveredSectionIndex(index)} + onMouseLeave={() => setHoveredSectionIndex(null)} + /> + + ); + })} + {unusedSpace > 0 && ( +
+ )} + {systemSection && ( + +
+ + )} +
+
+ {sections.map((section, index) => ( + +
setHoveredSectionIndex(index)} + onMouseLeave={() => setHoveredSectionIndex(null)} + > + + {section.name} +
+
+ ))} +
+
+ ); +}; + +export default StorageBar; diff --git a/interface/app/$libraryId/overview/index.tsx b/interface/app/$libraryId/overview/index.tsx index 656b4f86b..1c0b5bf4a 100644 --- a/interface/app/$libraryId/overview/index.tsx +++ b/interface/app/$libraryId/overview/index.tsx @@ -76,10 +76,9 @@ export const Component = () => {
- - + {node && ( { connectionType={null} /> )} - {/* - - - - */} { - {/* - */} - { // buttonText={t('connect_cloud)} /> - - {/* -
- {locations.map((location) => ( -
- - - {location.name} - -
- ))} -
-
*/}
diff --git a/interface/locales/en/common.json b/interface/locales/en/common.json index 274ea90dd..4dbc1f733 100644 --- a/interface/locales/en/common.json +++ b/interface/locales/en/common.json @@ -48,6 +48,7 @@ "backfill_sync_description": "Library is paused until backfill completes", "backups": "Backups", "backups_description": "Manage your Spacedrive database backups.", + "bar_graph_info": "Hover over each bar to see the file type. Double click to navigate.", "bitrate": "Bitrate", "blur_effects": "Blur Effects", "blur_effects_description": "Some components will have a blur effect applied to them.", @@ -711,10 +712,12 @@ "total_bytes_free_description": "Free space available on all nodes connected to the library.", "total_bytes_used": "Total used space", "total_bytes_used_description": "Total space used on all nodes connected to the library.", + "total_files": "Total files", "trash": "Trash", "type": "Type", "ui_animations": "UI Animations", "ui_animations_description": "Dialogs and other UI elements will animate when opening and closing.", + "unidentified_files_info": "Files that Spacedrive has been unable to identify.", "unknown": "Unknown", "unnamed_location": "Unnamed Location", "update": "Update", diff --git a/interface/package.json b/interface/package.json index 9e7d5686e..133278847 100644 --- a/interface/package.json +++ b/interface/package.json @@ -35,6 +35,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "crypto-random-string": "^5.0.0", + "d3-force": "^3.0.0", "dayjs": "^1.11.10", "framer-motion": "^10.16.4", "i18next": "^23.7.10", @@ -46,6 +47,7 @@ "react-colorful": "^5.6.1", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.11", + "react-force-graph-2d": "^1.25.5", "react-hook-form": "^7.47.0", "react-hotkeys-hook": "^4.4.1", "react-i18next": "^13.5.0", @@ -71,6 +73,7 @@ }, "devDependencies": { "@sd/config": "workspace:*", + "@types/d3-force": "^3.0.9", "@types/node": ">18.18.x", "@types/react": "^18.2.67", "@types/react-dom": "^18.2.22", diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 10025b8b4..9ad0996e0 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -383,9 +383,9 @@ export type JobProgressEvent = { id: string; library_id: string; task_count: num export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue } -export type KindStatistic = { kind: number; name: string; count: number; total_bytes: string } +export type KindStatistic = { kind: number; name: string; count: [number, number]; total_bytes: [number, number] } -export type KindStatistics = { statistics: KindStatistic[] } +export type KindStatistics = { statistics: KindStatistic[]; total_identified_files: number; total_unidentified_files: number } export type Label = { id: number; name: string; date_created: string | null; date_modified: string | null } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6f28c81e..829b3982a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,7 +147,7 @@ importers: version: 2.16.0 '@tauri-apps/cli': specifier: next - version: 2.0.0-beta.19 + version: 2.0.0-beta.20 '@types/react': specifier: ^18.2.67 version: 18.2.67 @@ -760,6 +760,9 @@ importers: crypto-random-string: specifier: ^5.0.0 version: 5.0.0 + d3-force: + specifier: ^3.0.0 + version: 3.0.0 dayjs: specifier: ^1.11.10 version: 1.11.10 @@ -793,6 +796,9 @@ importers: react-error-boundary: specifier: ^4.0.11 version: 4.0.13(react@18.2.0) + react-force-graph-2d: + specifier: ^1.25.5 + version: 1.25.5(react@18.2.0) react-hook-form: specifier: ^7.47.0 version: 7.51.1(react@18.2.0) @@ -863,6 +869,9 @@ importers: '@sd/config': specifier: workspace:* version: link:../packages/config + '@types/d3-force': + specifier: ^3.0.9 + version: 3.0.9 '@types/node': specifier: '>18.18.x' version: 20.11.29 @@ -3560,9 +3569,6 @@ packages: '@radix-ui/number@1.0.1': resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} - '@radix-ui/primitive@1.0.0': - resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==} - '@radix-ui/primitive@1.0.1': resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} @@ -3605,11 +3611,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-compose-refs@1.0.0': - resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-compose-refs@1.0.1': resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: @@ -3663,12 +3664,6 @@ packages: '@types/react': optional: true - '@radix-ui/react-dismissable-layer@1.0.2': - resolution: {integrity: sha512-WjJzMrTWROozDqLB0uRWYvj4UuXsM/2L19EmQ3Au+IJWqwvwq9Bwd+P8ivo0Deg9JDPArR1I6MbWNi1CmXsskg==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-dismissable-layer@1.0.4': resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==} peerDependencies: @@ -3843,12 +3838,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@1.0.1': - resolution: {integrity: sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-primitive@1.0.3': resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} peerDependencies: @@ -3927,11 +3916,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slot@1.0.1': - resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-slot@1.0.2': resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -3993,11 +3977,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-use-callback-ref@1.0.0': - resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-use-callback-ref@1.0.1': resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} peerDependencies: @@ -4016,11 +3995,6 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-escape-keydown@1.0.2': - resolution: {integrity: sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 - '@radix-ui/react-use-escape-keydown@1.0.3': resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} peerDependencies: @@ -4849,6 +4823,7 @@ packages: '@storybook/testing-library@0.2.2': resolution: {integrity: sha512-L8sXFJUHmrlyU2BsWWZGuAjv39Jl1uAqUHdxmN42JY15M4+XCMjGlArdCCjDe1wpTSW6USYISA9axjZojgtvnw==} + deprecated: In Storybook 8, this package functionality has been integrated to a new package called @storybook/test, which uses Vitest APIs for an improved experience. When upgrading to Storybook 8 with 'npx storybook@latest upgrade', you will get prompted and will get an automigration for the new package. Please migrate when you can. '@storybook/theming@8.0.1': resolution: {integrity: sha512-TUmSHRh3YrpJ25DYjD+9PpJaq9Qf9P1S2xpwfNARM9r2KpkMF1/RgqnnQgZpP9od0Tzvkji7XPzxPU//EmQKEA==} @@ -5118,68 +5093,68 @@ packages: resolution: {integrity: sha512-Np1opKANzRMF3lgJ9gDquBCB9SxlE2lRmNpVx1+L6RyzAmigkuh0ZulT5jMnDA3JLsuSDU135r/s4t/Pmx4atg==} engines: {node: '>= 18', npm: '>= 6.6.0', yarn: '>= 1.19.1'} - '@tauri-apps/cli-darwin-arm64@2.0.0-beta.19': - resolution: {integrity: sha512-c4KvyBnQ5C/P3oAyO7WZ71xYxW8yMwDe3I4Ik3Uz6+AXZ2k3xPx19VuxCgTJdJCkxtLvhAGu9Q2IZQuuDoGTsg==} + '@tauri-apps/cli-darwin-arm64@2.0.0-beta.20': + resolution: {integrity: sha512-oCJOCib7GuYkwkBXx+ekamR8NZZU+2i3MLP+DHpDxK5gS2uhCE+CBkamJkNt6y1x6xdVnwyqZOm5RvN4SRtyIA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.0.0-beta.19': - resolution: {integrity: sha512-t7rzloJwzgNXm82/w97Tq3RcvX7XmRcaxnu8ujV5SrREFxzLNRpkyzzr/vVthV7FZjKGcQf5QmJ3XeGXUfkCfQ==} + '@tauri-apps/cli-darwin-x64@2.0.0-beta.20': + resolution: {integrity: sha512-lC5QSnRExedYN4Ds6ZlSvC2PxP8qfIYBJQ5ktf+PJI5gQALdNeVtd6YnTG1ODCEklfLq9WKkGwp7JdALTU5wDA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.19': - resolution: {integrity: sha512-ZnM596ltSUNeBKH9rMGm1Ch1lCaeb1rW79nP1E6REuu1iOBpVAdkporaMWE7JSpkBZmSZdSuVDRhrMDuG7Uc6A==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.20': + resolution: {integrity: sha512-nZCeBMHHye5DLOJV5k2w658hnCS+LYaOZ8y/G9l3ei+g0L/HBjlSy6r4simsAT5TG8+l3oCZzLBngfTMdDS/YA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.19': - resolution: {integrity: sha512-xHAFx+6EqEKLQMrqQPwnzhygA2b/nn0b7pLF48YBvkDj3KLOmv5cC+K34f2l0KIaLB8B/oVFAQKsfet4XLew+w==} + '@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.20': + resolution: {integrity: sha512-B79ISVLPVBgwnCchVqwTKU+vxnFYqxKomcR4rmsvxfs0NVtT5QuNzE1k4NUQnw3966yjwhYR3mnHsSJQSB4Eyw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.19': - resolution: {integrity: sha512-ySRYhIfNDt/VXCycVt7d/dMBXf7L9iWf0SwynZ2nvJU/MaHIfJUgV68/l3RTRooYOCkYN4v/RRcGFD3wRmtE5g==} + '@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.20': + resolution: {integrity: sha512-ojIkv/1uZHhcrgfIN8xgn4BBeo/Xg+bnV0wer6lD78zyxkUMWeEZ+u3mae1ejCJNhhaZOxNaUQ67MvDOiGyr5Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.19': - resolution: {integrity: sha512-GEySXBulHQfGr3xuuv2ShnUrQtrWn3ynUtftoMiJNlpa1RTLfzglbUdA7zXag65E8h2jATVYnC/n8/sE5jtSHw==} + '@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.20': + resolution: {integrity: sha512-xBy1FNbHKlc7T6pOmFQQPECxJaI5A9QWX7Kb9N64cNVusoOGlvc3xHYkXMS4PTr7xXOT0yiE1Ww2OwDRJ3lYsg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-linux-x64-musl@2.0.0-beta.19': - resolution: {integrity: sha512-gz1x/7EhpMcIhUvR7RhG3D+dwUnXF+MIxPoiuDAKzQAj3i6qacZJvwxyRcpVQ6HaUDpmtaHz0AKpWIMmIFL90g==} + '@tauri-apps/cli-linux-x64-musl@2.0.0-beta.20': + resolution: {integrity: sha512-+O6zq5jmtUxA1FUAAwF2ywPysy4NRo2Y6G+ESZDkY9XosRwdt5OUjqAsYktZA3AxDMZVei8r9buwTqUwi9ny/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.19': - resolution: {integrity: sha512-Zz/UwU+7QQbz9lu9cpLzX/fCgmBG1lX+K5O97kTJVcqgBiS0zUc5q1efYr7ex4c6NLVP7uaUK3IKwctBy2MvEA==} + '@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.20': + resolution: {integrity: sha512-RswgMbWyOQcv53CHvIuiuhAh4kKDqaGyZfWD4VlxqX/XhkoF5gsNgr0MxzrY7pmoL+89oVI+fiGVJz4nOQE5vA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.19': - resolution: {integrity: sha512-fdT/u8I31PryeqULgzzUV+bYAlgt9WStJaZWt1/hMDffB9VViL3gO7V67mtNUEhBUMaX/SqItwklbJyy3TKXXg==} + '@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.20': + resolution: {integrity: sha512-5lgWmDVXhX3SBGbiv5SduM1yajiRnUEJClWhSdRrEEJeXdsxpCsBEhxYnUnDCEzPKxLLn5fdBv3VrVctJ03csQ==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.19': - resolution: {integrity: sha512-EHTi4D95mTmPC/MqWU5mBGhwZ0i82iVKEAAGaKDNdwYzibmioeANCzsD8eeyuU0kCE5BCWBYpA+2epGQnfDjMg==} + '@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.20': + resolution: {integrity: sha512-SuSiiVQTQPSzWlsxQp/NMzWbzDS9TdVDOw7CCfgiG5wnT2GsxzrcIAVN6i7ILsVFLxrjr0bIgPldSJcdcH84Yw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.0.0-beta.19': - resolution: {integrity: sha512-IHbgyUpnXY5ZEenQUz2Gce7w1Xl1BgLR6Jyf6SN0VbUVr9qJdSRPN7/FK+4JQFt2DC9076NVYTQFLOt03KNbwA==} + '@tauri-apps/cli@2.0.0-beta.20': + resolution: {integrity: sha512-707q9uIc2oNrYHd2dtMvxTrpZXVpart5EIktnRymNOpphkLlB6WUBjHD+ga45WqTU6cNGKbYvkKqTNfshNul9Q==} engines: {node: '>= 10'} hasBin: true @@ -5403,6 +5378,9 @@ packages: '@types/cross-spawn@6.0.6': resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + '@types/d3-force@3.0.9': + resolution: {integrity: sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -5866,6 +5844,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accessor-fn@1.5.0: + resolution: {integrity: sha512-dml7D96DY/K5lt4Ra2jMnpL9Bhw5HEGws4p1OAIxFFj9Utd/RxNfEO3T3f0QIWFNwQU7gNxH9snUfqF/zNkP/w==} + engines: {node: '>=12'} + acorn-import-assertions@1.9.0: resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} peerDependencies: @@ -6023,6 +6005,7 @@ packages: are-we-there-yet@1.1.7: resolution: {integrity: sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==} + deprecated: This package is no longer supported. arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -6275,6 +6258,9 @@ packages: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} + bezier-js@6.1.4: + resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==} + bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} @@ -6499,6 +6485,10 @@ packages: caniuse-lite@1.0.30001599: resolution: {integrity: sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==} + canvas-color-tracker@1.2.1: + resolution: {integrity: sha512-i5clg2pEdaWqHuEM/B74NZNLkHh5+OkXbA/T4iaBiaNDagkOCXkLNrhqUfdUugsRwuaNRU20e/OygzxWRor3yg==} + engines: {node: '>=12'} + caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -7019,6 +7009,86 @@ packages: engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} hasBin: true + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-binarytree@1.0.2: + resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-force-3d@3.0.5: + resolution: {integrity: sha512-tdwhAhoTYZY/a6eo9nR7HP3xSW/C6XvJTbeRpR92nlPzH6OiE+4MliN9feuSFd0tPtEUo+191qOhCTWx3NYifg==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-octree@1.0.2: + resolution: {integrity: sha512-Qxg4oirJrNXauiuC94uKMbgxwnhdda9xRLl9ihq45srlJ4Ga3CSgqGcAL8iW7N5CIv4Oz8x3E734ulxyvHPvwA==} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + dag-map@1.0.2: resolution: {integrity: sha512-+LSAiGFwQ9dRnRdOeaj7g47ZFJcOUPukAP8J3A3fuZ1g9Y44BG+P1sgApjLXTQPOzC4+7S9Wr8kXsfpINM4jpw==} @@ -8100,6 +8170,10 @@ packages: for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + force-graph@1.43.5: + resolution: {integrity: sha512-HveLELh9yhZXO/QOfaFS38vlwJZ/3sKu+jarfXzRmbmihSOH/BbRWnUvmg8wLFiYy6h4HlH4lkRfZRccHYmXgA==} + engines: {node: '>=12'} + foreach@2.0.6: resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==} @@ -8171,6 +8245,9 @@ packages: from@0.1.7: resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} + fromentries@1.3.2: + resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -8233,6 +8310,7 @@ packages: gauge@2.7.4: resolution: {integrity: sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==} + deprecated: This package is no longer supported. gensequence@7.0.0: resolution: {integrity: sha512-47Frx13aZh01afHJTB3zTtKIlFI6vWY+MYCN9Qpew6i52rfKjnhCF/l1YlC8UmEMvvntZZ6z4PiCcmyuedR2aQ==} @@ -8728,6 +8806,10 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + index-array-by@1.4.1: + resolution: {integrity: sha512-Zu6THdrxQdyTuT2uA5FjUoBEsFHPzHcPIj18FszN6yXKHxSfGcR4TPLabfuT//E25q1Igyx9xta2WMvD/x9P/g==} + engines: {node: '>=12'} + indexof@0.0.1: resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==} @@ -8740,6 +8822,7 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -8769,6 +8852,10 @@ packages: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + interpret@1.4.0: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} @@ -9124,6 +9211,10 @@ packages: engines: {node: '>=10'} hasBin: true + jerrypick@1.1.1: + resolution: {integrity: sha512-XTtedPYEyVp4t6hJrXuRKr/jHj8SC4z+4K0b396PMkov6muL+i8IIamJIvZWe3jUspgIJak0P+BaWKawMYNBLg==} + engines: {node: '>=12'} + jest-environment-node@29.7.0: resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -9274,6 +9365,10 @@ packages: jwt-decode@3.1.2: resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} + kapsule@1.14.5: + resolution: {integrity: sha512-H0iSpTynUzZw3tgraDmReprpFRmH5oP5GPmaNsurSwLx2H5iCpOMIkp5q+sfhB4Tz/UJd1E1IbEE9Z6ksnJ6RA==} + engines: {node: '>=12'} + katex@0.16.9: resolution: {integrity: sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==} hasBin: true @@ -9462,6 +9557,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -10353,6 +10451,7 @@ packages: npmlog@4.1.2: resolution: {integrity: sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==} + deprecated: This package is no longer supported. nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -10508,6 +10607,7 @@ packages: osenv@0.1.5: resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==} + deprecated: This package is no longer supported. ospath@1.2.2: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} @@ -11145,6 +11245,12 @@ packages: peerDependencies: react: '>=16.13.1' + react-force-graph-2d@1.25.5: + resolution: {integrity: sha512-3u8WjZZorpwZSDs3n3QeOS9ZoxFPM+IR9SStYJVQ/qKECydMHarxnf7ynV/MKJbC6kUsc60soD0V+Uq/r2vz7Q==} + engines: {node: '>=12'} + peerDependencies: + react: '*' + react-freeze@1.0.4: resolution: {integrity: sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==} engines: {node: '>=10'} @@ -11208,6 +11314,12 @@ packages: react: ^17.0.0 || ^16.3.0 || ^15.5.4 react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4 + react-kapsule@2.4.0: + resolution: {integrity: sha512-w4Yv9CgWdj8kWGQEPNWFGJJ08dYEZHZpiaFR/DgZjCMBNqv9wus2Gy1qvHVJmJbzvAZbq6jdvFC+NYzEqAlNhQ==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.13.1' + react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} @@ -11715,18 +11827,22 @@ packages: rimraf@2.4.5: resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@2.7.1: resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true ripemd160@2.0.2: @@ -12475,6 +12591,9 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyspy@2.2.1: resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} @@ -16333,10 +16452,6 @@ snapshots: dependencies: '@babel/runtime': 7.24.0 - '@radix-ui/primitive@1.0.0': - dependencies: - '@babel/runtime': 7.24.0 - '@radix-ui/primitive@1.0.1': dependencies: '@babel/runtime': 7.24.0 @@ -16381,11 +16496,6 @@ snapshots: '@types/react': 18.2.67 '@types/react-dom': 18.2.22 - '@radix-ui/react-compose-refs@1.0.0(react@18.2.0)': - dependencies: - '@babel/runtime': 7.24.0 - react: 18.2.0 - '@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.67)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.0 @@ -16445,17 +16555,6 @@ snapshots: optionalDependencies: '@types/react': 18.2.67 - '@radix-ui/react-dismissable-layer@1.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': - dependencies: - '@babel/runtime': 7.24.0 - '@radix-ui/primitive': 1.0.0 - '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0) - '@radix-ui/react-primitive': 1.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0) - '@radix-ui/react-use-escape-keydown': 1.0.2(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - '@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.0 @@ -16547,7 +16646,7 @@ snapshots: '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.67)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.67)(react@18.2.0) '@radix-ui/react-direction': 1.0.1(@types/react@18.2.67)(react@18.2.0) - '@radix-ui/react-dismissable-layer': 1.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.67)(react@18.2.0) '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-id': 1.0.1(@types/react@18.2.67)(react@18.2.0) @@ -16659,13 +16758,6 @@ snapshots: '@types/react': 18.2.67 '@types/react-dom': 18.2.22 - '@radix-ui/react-primitive@1.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': - dependencies: - '@babel/runtime': 7.24.0 - '@radix-ui/react-slot': 1.0.1(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.0 @@ -16774,12 +16866,6 @@ snapshots: '@types/react': 18.2.67 '@types/react-dom': 18.2.22 - '@radix-ui/react-slot@1.0.1(react@18.2.0)': - dependencies: - '@babel/runtime': 7.24.0 - '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0) - react: 18.2.0 - '@radix-ui/react-slot@1.0.2(@types/react@18.2.67)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.0 @@ -16863,11 +16949,6 @@ snapshots: '@types/react': 18.2.67 '@types/react-dom': 18.2.22 - '@radix-ui/react-use-callback-ref@1.0.0(react@18.2.0)': - dependencies: - '@babel/runtime': 7.24.0 - react: 18.2.0 - '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.67)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.0 @@ -16883,12 +16964,6 @@ snapshots: optionalDependencies: '@types/react': 18.2.67 - '@radix-ui/react-use-escape-keydown@1.0.2(react@18.2.0)': - dependencies: - '@babel/runtime': 7.24.0 - '@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0) - react: 18.2.0 - '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.67)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.0 @@ -18783,48 +18858,48 @@ snapshots: '@tauri-apps/api@2.0.0-beta.13': {} - '@tauri-apps/cli-darwin-arm64@2.0.0-beta.19': + '@tauri-apps/cli-darwin-arm64@2.0.0-beta.20': optional: true - '@tauri-apps/cli-darwin-x64@2.0.0-beta.19': + '@tauri-apps/cli-darwin-x64@2.0.0-beta.20': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.19': + '@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.20': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.19': + '@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.20': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.19': + '@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.20': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.19': + '@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.20': optional: true - '@tauri-apps/cli-linux-x64-musl@2.0.0-beta.19': + '@tauri-apps/cli-linux-x64-musl@2.0.0-beta.20': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.19': + '@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.20': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.19': + '@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.20': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.19': + '@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.20': optional: true - '@tauri-apps/cli@2.0.0-beta.19': + '@tauri-apps/cli@2.0.0-beta.20': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.0.0-beta.19 - '@tauri-apps/cli-darwin-x64': 2.0.0-beta.19 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.0.0-beta.19 - '@tauri-apps/cli-linux-arm64-gnu': 2.0.0-beta.19 - '@tauri-apps/cli-linux-arm64-musl': 2.0.0-beta.19 - '@tauri-apps/cli-linux-x64-gnu': 2.0.0-beta.19 - '@tauri-apps/cli-linux-x64-musl': 2.0.0-beta.19 - '@tauri-apps/cli-win32-arm64-msvc': 2.0.0-beta.19 - '@tauri-apps/cli-win32-ia32-msvc': 2.0.0-beta.19 - '@tauri-apps/cli-win32-x64-msvc': 2.0.0-beta.19 + '@tauri-apps/cli-darwin-arm64': 2.0.0-beta.20 + '@tauri-apps/cli-darwin-x64': 2.0.0-beta.20 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.0.0-beta.20 + '@tauri-apps/cli-linux-arm64-gnu': 2.0.0-beta.20 + '@tauri-apps/cli-linux-arm64-musl': 2.0.0-beta.20 + '@tauri-apps/cli-linux-x64-gnu': 2.0.0-beta.20 + '@tauri-apps/cli-linux-x64-musl': 2.0.0-beta.20 + '@tauri-apps/cli-win32-arm64-msvc': 2.0.0-beta.20 + '@tauri-apps/cli-win32-ia32-msvc': 2.0.0-beta.20 + '@tauri-apps/cli-win32-x64-msvc': 2.0.0-beta.20 '@tauri-apps/plugin-dialog@2.0.0-beta.3': dependencies: @@ -19125,6 +19200,8 @@ snapshots: dependencies: '@types/node': 20.11.29 + '@types/d3-force@3.0.9': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -19679,6 +19756,8 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accessor-fn@1.5.0: {} + acorn-import-assertions@1.9.0(acorn@8.11.3): dependencies: acorn: 8.11.3 @@ -20181,6 +20260,8 @@ snapshots: dependencies: open: 8.4.2 + bezier-js@6.1.4: {} + bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 @@ -20452,6 +20533,10 @@ snapshots: caniuse-lite@1.0.30001599: {} + canvas-color-tracker@1.2.1: + dependencies: + tinycolor2: 1.6.0 + caseless@0.12.0: {} ccount@2.0.1: {} @@ -21110,6 +21195,89 @@ snapshots: untildify: 4.0.0 yauzl: 2.10.0 + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-binarytree@1.0.2: {} + + d3-color@3.1.0: {} + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-ease@3.0.1: {} + + d3-force-3d@3.0.5: + dependencies: + d3-binarytree: 1.0.2 + d3-dispatch: 3.0.1 + d3-octree: 1.0.2 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-octree@1.0.2: {} + + d3-quadtree@3.0.1: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dag-map@1.0.2: {} damerau-levenshtein@1.0.8: {} @@ -22637,6 +22805,23 @@ snapshots: dependencies: is-callable: 1.2.7 + force-graph@1.43.5: + dependencies: + '@tweenjs/tween.js': 23.1.1 + accessor-fn: 1.5.0 + bezier-js: 6.1.4 + canvas-color-tracker: 1.2.1 + d3-array: 3.2.4 + d3-drag: 3.0.0 + d3-force-3d: 3.0.5 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + index-array-by: 1.4.1 + kapsule: 1.14.5 + lodash-es: 4.17.21 + foreach@2.0.6: {} foreground-child@3.1.1: @@ -22707,6 +22892,8 @@ snapshots: from@0.1.7: {} + fromentries@1.3.2: {} + fs-constants@1.0.0: {} fs-extra@10.1.0: @@ -23405,6 +23592,8 @@ snapshots: indent-string@4.0.0: {} + index-array-by@1.4.1: {} + indexof@0.0.1: {} infer-owner@1.0.4: {} @@ -23439,6 +23628,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.0.6 + internmap@2.0.3: {} + interpret@1.4.0: {} invariant@2.2.4: @@ -23725,6 +23916,8 @@ snapshots: filelist: 1.0.4 minimatch: 3.1.2 + jerrypick@1.1.1: {} + jest-environment-node@29.7.0: dependencies: '@jest/environment': 29.7.0 @@ -23935,6 +24128,10 @@ snapshots: jwt-decode@3.1.2: {} + kapsule@1.14.5: + dependencies: + lodash-es: 4.17.21 + katex@0.16.9: dependencies: commander: 8.3.0 @@ -24138,6 +24335,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.21: {} + lodash.camelcase@4.3.0: {} lodash.castarray@4.4.0: {} @@ -26358,6 +26557,13 @@ snapshots: '@babel/runtime': 7.24.0 react: 18.2.0 + react-force-graph-2d@1.25.5(react@18.2.0): + dependencies: + force-graph: 1.43.5 + prop-types: 15.8.1 + react: 18.2.0 + react-kapsule: 2.4.0(react@18.2.0) + react-freeze@1.0.4(react@18.2.0): dependencies: react: 18.2.0 @@ -26412,6 +26618,12 @@ snapshots: - '@types/react' - encoding + react-kapsule@2.4.0(react@18.2.0): + dependencies: + fromentries: 1.3.2 + jerrypick: 1.1.1 + react: 18.2.0 + react-lifecycles-compat@3.0.4: {} react-loading-icons@1.1.0: {} @@ -28120,6 +28332,8 @@ snapshots: tiny-invariant@1.3.3: {} + tinycolor2@1.6.0: {} + tinyspy@2.2.1: {} tmp@0.0.33: