diff --git a/apps/data-service/src/index.ts b/apps/data-service/src/index.ts
index a516a13..c3b1b10 100644
--- a/apps/data-service/src/index.ts
+++ b/apps/data-service/src/index.ts
@@ -22,10 +22,10 @@ const app = new Hono();
app.use(
'*',
cors({
- origin: ['http://localhost:4200', 'http://localhost:5173'],
- allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
+ origin: '*',
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
allowHeaders: ['Content-Type', 'Authorization'],
- credentials: true,
+ credentials: false,
})
);
diff --git a/apps/web/package.json b/apps/web/package.json
index 6d5563d..83b3112 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -13,6 +13,7 @@
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.0.18",
"@tanstack/react-table": "^8.21.3",
+ "@types/react-router-dom": "^5.3.3",
"@types/react-virtualized-auto-sizer": "^1.0.8",
"@types/react-window": "^1.8.8",
"clsx": "^2.1.1",
diff --git a/apps/web/src/app/App.tsx b/apps/web/src/app/App.tsx
index de4df77..e73c2b0 100644
--- a/apps/web/src/app/App.tsx
+++ b/apps/web/src/app/App.tsx
@@ -1,10 +1,31 @@
import { Layout } from '@/components/layout';
import { DashboardPage } from '@/features/dashboard';
+import { ExchangesPage } from '@/features/exchanges';
+import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
export function App() {
return (
-
-
-
+
+
+ }>
+ } />
+ } />
+ } />
+ Portfolio Page - Coming Soon}
+ />
+ Strategies Page - Coming Soon}
+ />
+ Analytics Page - Coming Soon}
+ />
+ Settings Page - Coming Soon} />
+
+
+
);
}
diff --git a/apps/web/src/components/layout/Layout.tsx b/apps/web/src/components/layout/Layout.tsx
index e11b343..24b711f 100644
--- a/apps/web/src/components/layout/Layout.tsx
+++ b/apps/web/src/components/layout/Layout.tsx
@@ -1,22 +1,28 @@
-import { ReactNode, useState } from 'react';
+import { useState } from 'react';
+import { Outlet, useLocation } from 'react-router-dom';
import { Header } from './Header';
import { Sidebar } from './Sidebar';
-interface LayoutProps {
- children: ReactNode;
- title?: string;
-}
-
-export function Layout({ children, title }: LayoutProps) {
+export function Layout() {
const [sidebarOpen, setSidebarOpen] = useState(false);
+ const location = useLocation();
+
+ // Determine title from current route
+ const getTitle = () => {
+ const path = location.pathname.replace('/', '');
+ if (!path || path === 'dashboard') return 'Dashboard';
+ return path.charAt(0).toUpperCase() + path.slice(1);
+ };
return (
-
+
-
- {children}
+
+
+
+
);
diff --git a/apps/web/src/components/layout/Sidebar.tsx b/apps/web/src/components/layout/Sidebar.tsx
index b4263dc..f6822b0 100644
--- a/apps/web/src/components/layout/Sidebar.tsx
+++ b/apps/web/src/components/layout/Sidebar.tsx
@@ -3,6 +3,7 @@ import { cn } from '@/lib/utils';
import { Dialog, Transition } from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { Fragment } from 'react';
+import { NavLink } from 'react-router-dom';
interface SidebarProps {
sidebarOpen: boolean;
@@ -86,26 +87,32 @@ function SidebarContent() {
diff --git a/apps/web/src/components/ui/DataTable/DataTable.tsx b/apps/web/src/components/ui/DataTable/DataTable.tsx
index 64d87e2..7e9e164 100644
--- a/apps/web/src/components/ui/DataTable/DataTable.tsx
+++ b/apps/web/src/components/ui/DataTable/DataTable.tsx
@@ -80,12 +80,12 @@ export function DataTable({
Table: ({ style, ...props }) => (
diff --git a/apps/web/src/features/exchanges/ExchangesPage.tsx b/apps/web/src/features/exchanges/ExchangesPage.tsx
new file mode 100644
index 0000000..4dc71cc
--- /dev/null
+++ b/apps/web/src/features/exchanges/ExchangesPage.tsx
@@ -0,0 +1,16 @@
+import { ExchangesTable } from './components/ExchangesTable';
+
+export function ExchangesPage() {
+ return (
+
+
+
Exchange Management
+
+ Configure and manage master exchanges with their data sources and providers.
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/features/exchanges/components/AddSourceDialog.tsx b/apps/web/src/features/exchanges/components/AddSourceDialog.tsx
new file mode 100644
index 0000000..70d2f57
--- /dev/null
+++ b/apps/web/src/features/exchanges/components/AddSourceDialog.tsx
@@ -0,0 +1,198 @@
+import { Dialog, Transition } from '@headlessui/react';
+import { XMarkIcon } from '@heroicons/react/24/outline';
+import React, { useState } from 'react';
+import { AddSourceRequest } from '../types';
+
+interface AddSourceDialogProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onAddSource: (request: AddSourceRequest) => Promise;
+ exchangeId: string;
+ exchangeName: string;
+}
+
+export function AddSourceDialog({
+ isOpen,
+ onClose,
+ onAddSource,
+ exchangeId,
+ exchangeName,
+}: AddSourceDialogProps) {
+ const [source, setSource] = useState('');
+ const [id, setId] = useState('');
+ const [name, setName] = useState('');
+ const [code, setCode] = useState('');
+ const [aliases, setAliases] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!source || !id || !name || !code) return;
+
+ setLoading(true);
+ try {
+ await onAddSource({
+ source,
+ mapping: {
+ id,
+ name,
+ code,
+ aliases: aliases
+ .split(',')
+ .map(a => a.trim())
+ .filter(Boolean),
+ },
+ });
+
+ // Reset form
+ setSource('');
+ setId('');
+ setName('');
+ setCode('');
+ setAliases('');
+ } catch (error) {
+ console.error('Error adding source:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/features/exchanges/components/ExchangesTable.tsx b/apps/web/src/features/exchanges/components/ExchangesTable.tsx
new file mode 100644
index 0000000..d604bd6
--- /dev/null
+++ b/apps/web/src/features/exchanges/components/ExchangesTable.tsx
@@ -0,0 +1,292 @@
+import { DataTable } from '@/components/ui';
+import { PlusIcon, XMarkIcon } from '@heroicons/react/24/outline';
+import { ColumnDef } from '@tanstack/react-table';
+import { useCallback, useMemo, useState } from 'react';
+import { useExchanges } from '../hooks/useExchanges';
+import { Exchange, SourceMapping } from '../types';
+import { AddSourceDialog } from './AddSourceDialog';
+
+export function ExchangesTable() {
+ const { exchanges, loading, error, updateExchange, addSource, removeSource } = useExchanges();
+ const [editingCell, setEditingCell] = useState<{ id: string; field: string } | null>(null);
+ const [editValue, setEditValue] = useState('');
+ const [addSourceDialog, setAddSourceDialog] = useState<{
+ id: string;
+ exchangeName: string;
+ } | null>(null);
+
+ const handleCellEdit = useCallback(
+ async (id: string, field: string, value: string) => {
+ if (field === 'shortName') {
+ await updateExchange(id, { shortName: value });
+ }
+ setEditingCell(null);
+ setEditValue('');
+ },
+ [updateExchange]
+ );
+
+ const handleToggleActive = useCallback(
+ async (id: string, currentStatus: boolean) => {
+ await updateExchange(id, { active: !currentStatus });
+ },
+ [updateExchange]
+ );
+
+ const handleAddSource = useCallback(async (id: string, exchangeName: string) => {
+ setAddSourceDialog({ id, exchangeName });
+ }, []);
+
+ const handleRemoveSource = useCallback(
+ async (exchangeId: string, sourceName: string) => {
+ if (confirm(`Are you sure you want to remove the ${sourceName} source?`)) {
+ await removeSource(exchangeId, sourceName);
+ }
+ },
+ [removeSource]
+ );
+
+ const columns = useMemo[]>(() => {
+ return [
+ {
+ id: 'masterExchangeId',
+ header: 'Master ID',
+ accessorKey: 'masterExchangeId',
+ size: 50,
+ enableResizing: false,
+ cell: ({ getValue, cell }) => (
+
+ {getValue() as string}
+
+ ),
+ },
+ {
+ id: 'shortName',
+ header: 'Short Name',
+ accessorKey: 'shortName',
+ size: 50,
+ enableResizing: false,
+ cell: ({ getValue, row, cell }) => {
+ const isEditing =
+ editingCell?.id === row.original._id && editingCell?.field === 'shortName';
+
+ if (isEditing) {
+ return (
+ setEditValue(e.target.value)}
+ onBlur={() => handleCellEdit(row.original._id, 'shortName', editValue)}
+ onKeyDown={e => {
+ if (e.key === 'Enter') {
+ handleCellEdit(row.original._id, 'shortName', editValue);
+ } else if (e.key === 'Escape') {
+ setEditingCell(null);
+ setEditValue('');
+ }
+ }}
+ className="w-full bg-surface border border-border rounded px-2 py-1 text-sm"
+ autoFocus
+ />
+ );
+ }
+
+ return (
+ {
+ setEditingCell({ id: row.original._id, field: 'shortName' });
+ setEditValue(getValue() as string);
+ }}
+ >
+ {getValue() as string}
+
+ );
+ },
+ },
+ {
+ id: 'officialName',
+ header: 'Official Name',
+ accessorKey: 'officialName',
+ size: 150,
+ maxSize: 150,
+ enableResizing: true,
+ cell: ({ getValue, cell }) => (
+
+ {getValue() as string}
+
+ ),
+ },
+ {
+ id: 'country',
+ header: 'Country',
+ accessorKey: 'country',
+ size: 40,
+ maxSize: 40,
+ cell: ({ getValue }) => (
+ {getValue() as string}
+ ),
+ },
+ {
+ id: 'currency',
+ header: 'Currency',
+ accessorKey: 'currency',
+ size: 40,
+ cell: ({ getValue, cell }) => (
+
+ {getValue() as string}
+
+ ),
+ },
+ {
+ id: 'active',
+ header: 'Active',
+ accessorKey: 'active',
+ size: 80,
+ maxSize: 80,
+ cell: ({ getValue, row, cell }) => {
+ const isActive = getValue() as boolean;
+ return (
+
+ );
+ },
+ },
+ {
+ id: 'sources',
+ header: 'Sources',
+ accessorKey: 'sourceMappings',
+ minSize: 400,
+ maxSize: 400,
+ size: 400,
+ enableResizing: true,
+ cell: ({ getValue, row, cell }) => {
+ const sourceMappings = getValue() as Record;
+ const sources = Object.keys(sourceMappings);
+
+ return (
+
+ {sources.map(source => (
+
+ {source.toUpperCase()}
+
+
+ ))}
+
+
+ );
+ },
+ },
+ {
+ id: 'updated_at',
+ header: 'Last Updated',
+ accessorKey: 'updated_at',
+ size: 150,
+ maxSize: 150,
+ cell: ({ getValue }) => (
+
+ {new Date(getValue() as string).toLocaleDateString()}
+
+ ),
+ },
+ ];
+ }, [
+ editingCell,
+ editValue,
+ handleCellEdit,
+ handleRemoveSource,
+ handleToggleActive,
+ handleAddSource,
+ ]);
+
+ if (error) {
+ return (
+
+
Error Loading Exchanges
+
{error}
+
+ Make sure the data-service is running on localhost:2001
+
+
+ );
+ }
+
+ return (
+
+
+
+
Exchanges Management
+
+ Manage exchange configurations and source mappings
+
+
+
{exchanges?.length || 0} exchanges loaded
+
+
+
+
+
+ Debug: Data length: {exchanges?.length || 0}, Loading: {loading.toString()}, Error:{' '}
+ {error || 'none'}
+
+
+ {addSourceDialog && (
+
setAddSourceDialog(null)}
+ onAddSource={async (sourceRequest: {
+ source: string;
+ mapping: { id: string; name: string; code: string; aliases: string[] };
+ }) => {
+ const success = await addSource(addSourceDialog.id, sourceRequest);
+ if (success) {
+ setAddSourceDialog(null);
+ }
+ }}
+ />
+ )}
+
+ );
+}
diff --git a/apps/web/src/features/exchanges/components/index.ts b/apps/web/src/features/exchanges/components/index.ts
new file mode 100644
index 0000000..f056547
--- /dev/null
+++ b/apps/web/src/features/exchanges/components/index.ts
@@ -0,0 +1,2 @@
+export { AddSourceDialog } from './AddSourceDialog';
+export { ExchangesTable } from './ExchangesTable';
diff --git a/apps/web/src/features/exchanges/hooks/index.ts b/apps/web/src/features/exchanges/hooks/index.ts
new file mode 100644
index 0000000..7cc97c9
--- /dev/null
+++ b/apps/web/src/features/exchanges/hooks/index.ts
@@ -0,0 +1 @@
+export { useExchanges } from './useExchanges';
diff --git a/apps/web/src/features/exchanges/hooks/useExchanges.ts b/apps/web/src/features/exchanges/hooks/useExchanges.ts
new file mode 100644
index 0000000..b0ef638
--- /dev/null
+++ b/apps/web/src/features/exchanges/hooks/useExchanges.ts
@@ -0,0 +1,132 @@
+import { useCallback, useEffect, useState } from 'react';
+import { AddSourceRequest, Exchange, UpdateExchangeRequest } from '../types';
+
+const API_BASE_URL = 'http://localhost:2001/api';
+
+export function useExchanges() {
+ const [exchanges, setExchanges] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const fetchExchanges = useCallback(async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ const response = await fetch(`${API_BASE_URL}/exchanges`);
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch exchanges: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+
+ if (data.status === 'success') {
+ // The API returns exchanges directly in data.data array
+ setExchanges(data.data || []);
+ } else {
+ throw new Error('API returned error status');
+ }
+ } catch (err) {
+ console.error('Error fetching exchanges:', err);
+ setError(err instanceof Error ? err.message : 'Failed to fetch exchanges');
+ setExchanges([]); // Reset to empty array on error
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const updateExchange = useCallback(
+ async (id: string, updates: UpdateExchangeRequest) => {
+ try {
+ const response = await fetch(`${API_BASE_URL}/exchanges/${id}`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(updates),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to update exchange: ${response.statusText}`);
+ }
+
+ // Refresh the exchanges list
+ await fetchExchanges();
+ return true;
+ } catch (err) {
+ console.error('Error updating exchange:', err);
+ setError(err instanceof Error ? err.message : 'Failed to update exchange');
+ return false;
+ }
+ },
+ [fetchExchanges]
+ );
+
+ const addSource = useCallback(
+ async (exchangeId: string, sourceRequest: AddSourceRequest) => {
+ try {
+ const response = await fetch(`${API_BASE_URL}/exchanges/${exchangeId}/sources`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(sourceRequest),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to add source: ${response.statusText}`);
+ }
+
+ // Refresh the exchanges list
+ await fetchExchanges();
+ return true;
+ } catch (err) {
+ console.error('Error adding source:', err);
+ setError(err instanceof Error ? err.message : 'Failed to add source');
+ return false;
+ }
+ },
+ [fetchExchanges]
+ );
+
+ const removeSource = useCallback(
+ async (exchangeId: string, sourceName: string) => {
+ try {
+ const response = await fetch(
+ `${API_BASE_URL}/exchanges/${exchangeId}/sources/${sourceName}`,
+ {
+ method: 'DELETE',
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`Failed to remove source: ${response.statusText}`);
+ }
+
+ // Refresh the exchanges list
+ await fetchExchanges();
+ return true;
+ } catch (err) {
+ console.error('Error removing source:', err);
+ setError(err instanceof Error ? err.message : 'Failed to remove source');
+ return false;
+ }
+ },
+ [fetchExchanges]
+ );
+
+ useEffect(() => {
+ fetchExchanges();
+ }, [fetchExchanges]);
+
+ return {
+ exchanges,
+ loading,
+ error,
+ refetch: fetchExchanges,
+ updateExchange,
+ addSource,
+ removeSource,
+ };
+}
diff --git a/apps/web/src/features/exchanges/index.ts b/apps/web/src/features/exchanges/index.ts
new file mode 100644
index 0000000..a5be8e2
--- /dev/null
+++ b/apps/web/src/features/exchanges/index.ts
@@ -0,0 +1,4 @@
+export * from './components';
+export { ExchangesPage } from './ExchangesPage';
+export * from './hooks';
+export * from './types';
diff --git a/apps/web/src/features/exchanges/types/index.ts b/apps/web/src/features/exchanges/types/index.ts
new file mode 100644
index 0000000..7c9574c
--- /dev/null
+++ b/apps/web/src/features/exchanges/types/index.ts
@@ -0,0 +1,45 @@
+export interface SourceMapping {
+ id: string;
+ name: string;
+ code: string;
+ aliases: string[];
+ lastUpdated: string;
+}
+
+export interface Exchange {
+ _id: string;
+ masterExchangeId: string;
+ shortName: string;
+ officialName: string;
+ country: string;
+ currency: string;
+ timezone: string;
+ active: boolean;
+ sourceMappings: Record;
+ confidence: number;
+ verified: boolean;
+ source: string;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface ExchangesApiResponse {
+ status: string;
+ data: Exchange[]; // Exchanges are directly in data array
+ count: number;
+}
+
+export interface UpdateExchangeRequest {
+ shortName?: string;
+ active?: boolean;
+}
+
+export interface AddSourceRequest {
+ source: string;
+ mapping: {
+ id: string;
+ name: string;
+ code: string;
+ aliases: string[];
+ };
+}
diff --git a/apps/web/src/lib/constants.ts b/apps/web/src/lib/constants.ts
new file mode 100644
index 0000000..f1478d5
--- /dev/null
+++ b/apps/web/src/lib/constants.ts
@@ -0,0 +1,17 @@
+import {
+ BuildingLibraryIcon,
+ ChartBarIcon,
+ CogIcon,
+ DocumentTextIcon,
+ HomeIcon,
+ PresentationChartLineIcon,
+} from '@heroicons/react/24/outline';
+
+export const navigation = [
+ { name: 'Dashboard', href: '/dashboard', icon: HomeIcon },
+ { name: 'Exchanges', href: '/exchanges', icon: BuildingLibraryIcon },
+ { name: 'Portfolio', href: '/portfolio', icon: ChartBarIcon },
+ { name: 'Strategies', href: '/strategies', icon: DocumentTextIcon },
+ { name: 'Analytics', href: '/analytics', icon: PresentationChartLineIcon },
+ { name: 'Settings', href: '/settings', icon: CogIcon },
+];
diff --git a/apps/web/src/lib/constants/navigation.ts b/apps/web/src/lib/constants/navigation.ts
index f0384cf..3bd3353 100644
--- a/apps/web/src/lib/constants/navigation.ts
+++ b/apps/web/src/lib/constants/navigation.ts
@@ -1,4 +1,5 @@
import {
+ BuildingOfficeIcon,
ChartBarIcon,
CogIcon,
CurrencyDollarIcon,
@@ -25,6 +26,12 @@ export const navigation = [
icon: ChartBarIcon,
current: false,
},
+ {
+ name: 'Exchanges',
+ href: '/exchanges',
+ icon: BuildingOfficeIcon,
+ current: false,
+ },
{
name: 'Reports',
href: '/reports',
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts
new file mode 100644
index 0000000..6b52e6e
--- /dev/null
+++ b/apps/web/src/lib/utils.ts
@@ -0,0 +1,25 @@
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+
+// Utility functions for financial data formatting
+export function formatCurrency(value: number): string {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 2,
+ }).format(value);
+}
+
+export function formatPercentage(value: number): string {
+ return `${value >= 0 ? '+' : ''}${value.toFixed(2)}%`;
+}
+
+export function getValueColor(value: number): string {
+ if (value > 0) return 'text-success';
+ if (value < 0) return 'text-danger';
+ return 'text-text-secondary';
+}
diff --git a/bun.lock b/bun.lock
index a623467..b155d09 100644
--- a/bun.lock
+++ b/bun.lock
@@ -143,6 +143,7 @@
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.0.18",
"@tanstack/react-table": "^8.21.3",
+ "@types/react-router-dom": "^5.3.3",
"@types/react-virtualized-auto-sizer": "^1.0.8",
"@types/react-window": "^1.8.8",
"clsx": "^2.1.1",
@@ -750,6 +751,8 @@
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
+ "@types/history": ["@types/history@4.7.11", "", {}, "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA=="],
+
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
@@ -770,6 +773,10 @@
"@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
+ "@types/react-router": ["@types/react-router@5.1.20", "", { "dependencies": { "@types/history": "^4.7.11", "@types/react": "*" } }, "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q=="],
+
+ "@types/react-router-dom": ["@types/react-router-dom@5.3.3", "", { "dependencies": { "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router": "*" } }, "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw=="],
+
"@types/react-virtualized-auto-sizer": ["@types/react-virtualized-auto-sizer@1.0.8", "", { "dependencies": { "react-virtualized-auto-sizer": "*" } }, "sha512-keJpNyhiwfl2+N12G1ocCVA5ZDBArbPLe/S90X3kt7fam9naeHdaYYWbpe2sHczp70JWJ+2QLhBE8kLvLuVNjA=="],
"@types/react-window": ["@types/react-window@1.8.8", "", { "dependencies": { "@types/react": "*" } }, "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q=="],