diff --git a/packages/component-library/.storybook/storybook.module.scss b/packages/component-library/.storybook/storybook.module.scss index 4ee4766cc3..5b1a8f8469 100644 --- a/packages/component-library/.storybook/storybook.module.scss +++ b/packages/component-library/.storybook/storybook.module.scss @@ -6,7 +6,7 @@ body, } .contents { - height: 100vh; + min-height: 100vh; width: 100vw; color: theme.color("foreground"); background: theme.color("background", "primary"); diff --git a/packages/component-library/package.json b/packages/component-library/package.json index 5ddfe06f54..2e54abadb6 100644 --- a/packages/component-library/package.json +++ b/packages/component-library/package.json @@ -45,6 +45,8 @@ "@amplitude/plugin-autocapture-browser": "catalog:", "@axe-core/react": "catalog:", "@next/third-parties": "catalog:", + "ag-grid-community": "catalog:", + "ag-grid-react": "catalog:", "@react-hookz/web": "catalog:", "bcp-47": "catalog:", "clsx": "catalog:", diff --git a/packages/component-library/src/SymbolPairTag/index.stories.tsx b/packages/component-library/src/SymbolPairTag/index.stories.tsx index ed0e05400b..2618fa61e5 100644 --- a/packages/component-library/src/SymbolPairTag/index.stories.tsx +++ b/packages/component-library/src/SymbolPairTag/index.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import CryptoIcon from "cryptocurrency-icons/svg/color/btc.svg"; +import BtcIcon from "cryptocurrency-icons/svg/color/btc.svg"; import { SymbolPairTag as SymbolPairTagComponent } from "./index.jsx"; const meta = { @@ -29,7 +29,7 @@ export const SymbolPairTag = { args: { displaySymbol: "BTC/USD", isLoading: false, - icon: , + icon: , description: "Bitcoin", }, } satisfies StoryObj; diff --git a/packages/component-library/src/TableGrid/dummy-row-data.ts b/packages/component-library/src/TableGrid/dummy-row-data.ts new file mode 100644 index 0000000000..f4fba3b3a3 --- /dev/null +++ b/packages/component-library/src/TableGrid/dummy-row-data.ts @@ -0,0 +1,89 @@ +export const dummyRowData = [ + { id: 1, feed: "BTC/USD", price: 54_392.28, confidence: 4.31 }, + { id: 2, feed: "ETH/USD", price: 3129.5, confidence: 7.92 }, + { id: 3, feed: "SOL/USD", price: 142.11, confidence: 2.48 }, + { id: 4, feed: "DOGE/USD", price: 0.21, confidence: 8.17 }, + { id: 5, feed: "BNB/USD", price: 587.44, confidence: 3.22 }, + { id: 6, feed: "XRP/USD", price: 0.76, confidence: 6.43 }, + { id: 7, feed: "ADA/USD", price: 1.42, confidence: 9.88 }, + { id: 8, feed: "DOT/USD", price: 32.67, confidence: 1.76 }, + { id: 9, feed: "MATIC/USD", price: 2.04, confidence: 12.11 }, + { id: 10, feed: "ATOM/USD", price: 14.55, confidence: 4.98 }, + + { id: 11, feed: "LTC/USD", price: 228.77, confidence: 5.66 }, + { id: 12, feed: "TRX/USD", price: 0.12, confidence: 6.44 }, + { id: 13, feed: "NEAR/USD", price: 9.83, confidence: 7.15 }, + { id: 14, feed: "APT/USD", price: 11.25, confidence: 3.9 }, + { id: 15, feed: "SUI/USD", price: 1.77, confidence: 9.28 }, + { id: 16, feed: "ARB/USD", price: 1.54, confidence: 11.32 }, + { id: 17, feed: "OP/USD", price: 2.12, confidence: 10.44 }, + { id: 18, feed: "FIL/USD", price: 36.8, confidence: 2.27 }, + { id: 19, feed: "ICP/USD", price: 18.93, confidence: 6.15 }, + { id: 20, feed: "BTC/USD", price: 61_034.72, confidence: 4.08 }, + + { id: 21, feed: "ETH/USD", price: 2940.18, confidence: 7.54 }, + { id: 22, feed: "SOL/USD", price: 153.22, confidence: 2.17 }, + { id: 23, feed: "DOGE/USD", price: 0.25, confidence: 11.28 }, + { id: 24, feed: "BNB/USD", price: 612.93, confidence: 3.1 }, + { id: 25, feed: "XRP/USD", price: 0.79, confidence: 5.94 }, + { id: 26, feed: "ADA/USD", price: 1.52, confidence: 9.45 }, + { id: 27, feed: "DOT/USD", price: 28.65, confidence: 1.93 }, + { id: 28, feed: "MATIC/USD", price: 2.14, confidence: 11.57 }, + { id: 29, feed: "ATOM/USD", price: 13.72, confidence: 5.36 }, + { id: 30, feed: "LTC/USD", price: 239.85, confidence: 4.79 }, + + { id: 31, feed: "TRX/USD", price: 0.11, confidence: 6.72 }, + { id: 32, feed: "NEAR/USD", price: 10.44, confidence: 7.09 }, + { id: 33, feed: "APT/USD", price: 10.85, confidence: 4.02 }, + { id: 34, feed: "SUI/USD", price: 1.83, confidence: 9.91 }, + { id: 35, feed: "ARB/USD", price: 1.61, confidence: 10.75 }, + { id: 36, feed: "OP/USD", price: 2.25, confidence: 8.66 }, + { id: 37, feed: "FIL/USD", price: 34.72, confidence: 3.48 }, + { id: 38, feed: "ICP/USD", price: 19.14, confidence: 5.28 }, + { id: 39, feed: "BTC/USD", price: 57_902.41, confidence: 3.76 }, + { id: 40, feed: "ETH/USD", price: 3055.91, confidence: 7.11 }, + + { id: 41, feed: "SOL/USD", price: 161.83, confidence: 1.99 }, + { id: 42, feed: "DOGE/USD", price: 0.19, confidence: 8.74 }, + { id: 43, feed: "BNB/USD", price: 595.27, confidence: 3.85 }, + { id: 44, feed: "XRP/USD", price: 0.73, confidence: 6.2 }, + { id: 45, feed: "ADA/USD", price: 1.48, confidence: 10.51 }, + { id: 46, feed: "DOT/USD", price: 30.12, confidence: 2.09 }, + { id: 47, feed: "MATIC/USD", price: 2.19, confidence: 12.88 }, + { id: 48, feed: "ATOM/USD", price: 14.04, confidence: 4.62 }, + { id: 49, feed: "LTC/USD", price: 244.31, confidence: 5.12 }, + { id: 50, feed: "TRX/USD", price: 0.13, confidence: 6.91 }, + + { id: 51, feed: "NEAR/USD", price: 9.95, confidence: 6.84 }, + { id: 52, feed: "APT/USD", price: 11.42, confidence: 4.44 }, + { id: 53, feed: "SUI/USD", price: 1.79, confidence: 8.93 }, + { id: 54, feed: "ARB/USD", price: 1.59, confidence: 10.13 }, + { id: 55, feed: "OP/USD", price: 2.31, confidence: 9.2 }, + { id: 56, feed: "FIL/USD", price: 37.02, confidence: 2.72 }, + { id: 57, feed: "ICP/USD", price: 18.67, confidence: 6.01 }, + { id: 58, feed: "BTC/USD", price: 59_243.82, confidence: 3.92 }, + { id: 59, feed: "ETH/USD", price: 3089.27, confidence: 7.47 }, + { id: 60, feed: "SOL/USD", price: 158.71, confidence: 2.26 }, + + { id: 61, feed: "DOGE/USD", price: 0.22, confidence: 8.55 }, + { id: 62, feed: "BNB/USD", price: 601.35, confidence: 3.69 }, + { id: 63, feed: "XRP/USD", price: 0.75, confidence: 5.72 }, + { id: 64, feed: "ADA/USD", price: 1.55, confidence: 9.68 }, + { id: 65, feed: "DOT/USD", price: 31.44, confidence: 1.88 }, + { id: 66, feed: "MATIC/USD", price: 2.23, confidence: 11.83 }, + { id: 67, feed: "ATOM/USD", price: 14.88, confidence: 4.34 }, + { id: 68, feed: "LTC/USD", price: 237.72, confidence: 5.55 }, + { id: 69, feed: "TRX/USD", price: 0.14, confidence: 6.63 }, + { id: 70, feed: "NEAR/USD", price: 10.17, confidence: 7.03 }, + + { id: 71, feed: "APT/USD", price: 11.07, confidence: 3.71 }, + { id: 72, feed: "SUI/USD", price: 1.81, confidence: 9.44 }, + { id: 73, feed: "ARB/USD", price: 1.63, confidence: 11.09 }, + { id: 74, feed: "OP/USD", price: 2.19, confidence: 10.27 }, + { id: 75, feed: "FIL/USD", price: 35.91, confidence: 3.18 }, + { id: 76, feed: "ICP/USD", price: 19.35, confidence: 5.64 }, + { id: 77, feed: "BTC/USD", price: 60_517.49, confidence: 4.02 }, + { id: 78, feed: "ETH/USD", price: 3112.84, confidence: 7.21 }, + { id: 79, feed: "SOL/USD", price: 149.26, confidence: 2.51 }, + { id: 80, feed: "DOGE/USD", price: 0.2, confidence: 9.12 }, +]; diff --git a/packages/component-library/src/TableGrid/index.module.scss b/packages/component-library/src/TableGrid/index.module.scss new file mode 100644 index 0000000000..d345325401 --- /dev/null +++ b/packages/component-library/src/TableGrid/index.module.scss @@ -0,0 +1,28 @@ +@use "../theme"; + +.tableGrid { + --ag-browser-color-scheme: light-dark(light, dark); + --ag-background-color: #{theme.color("background", "primary")}; + --ag-header-background-color: var(--ag-background-color); + --ag-foreground-color: #{theme.color("paragraph")}; + --ag-accent-color: #{theme.color("button", "outline", "background", "active")}; + --ag-header-font-size: #{theme.font-size("xs")}; + --ag-header-font-weight: #{theme.font-weight("medium")}; + --ag-cell-font-size: #{theme.font-size("xs")}; + --ag-wrapper-border: none; + --ag-cell-text-color: #{theme.color("paragraph")}; + --ag-data-font-size: #{theme.font-size("base")}; + + height: 100%; +} + +.defaultCellContainer { + height: 100%; + display: flex; + align-items: center; +} + +.skeletonContainer { + height: theme.spacing(10); + width: 100%; +} diff --git a/packages/component-library/src/TableGrid/index.stories.tsx b/packages/component-library/src/TableGrid/index.stories.tsx new file mode 100644 index 0000000000..bf54092d4c --- /dev/null +++ b/packages/component-library/src/TableGrid/index.stories.tsx @@ -0,0 +1,129 @@ +import { ChartLine } from "@phosphor-icons/react/dist/ssr/ChartLine"; +import type { Meta, StoryObj } from "@storybook/react"; +import BtcIcon from "cryptocurrency-icons/svg/color/btc.svg"; + +import { Badge } from "../Badge"; +import { SymbolPairTag } from "../SymbolPairTag"; +import { dummyRowData } from "./dummy-row-data"; +import { TableGrid as TableGridComponent } from "./index.jsx"; + +const meta = { + component: TableGridComponent, + parameters: { + layout: "padded", + }, + argTypes: { + columnDefs: { + table: { + disable: true, + }, + }, + rowData: { + table: { + disable: true, + }, + }, + className: { + table: { + disable: true, + }, + }, + cardProps: { + table: { + category: "Outer Card", + }, + }, + loading: { + control: "boolean", + table: { + category: "State", + }, + }, + }, +} satisfies Meta; +export default meta; + +const PriceCellRenderer = ({ value }: { value: number }) => ( + + {`$${value.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`} + +); + +const ConfidenceCellRenderer = ({ value }: { value: number }) => ( + {`+/- ${value.toFixed(2)}%`} +); + +const FeedCellRenderer = ({ value }: { value: string }) => ( +
+ } + description={value} + /> +
+); + +const FeedCellRendererLoading = () => ( +
+ +
+); + +const args = { + columnDefs: [ + { + headerName: "ID", + field: "id", + }, + { + headerName: "PRICE FEED", + field: "feed", + cellRenderer: FeedCellRenderer, + loadingCellRenderer: FeedCellRendererLoading, + flex: 2, + }, + { + headerName: "PRICE", + field: "price", + flex: 3, + cellRenderer: PriceCellRenderer, + }, + { + headerName: "CONFIDENCE", + field: "confidence", + cellRenderer: ConfidenceCellRenderer, + }, + ], + rowHeight: 70, + rowData: dummyRowData, +}; + +export const TableGrid = { + args, +} satisfies StoryObj; + +export const PriceFeedsCard = { + render: (props) => { + return ; + }, + args: { + ...args, + pagination: true, + cardProps: { + icon: , + title: ( + <> + Price Feeds + + {args.rowData.length} + + + ), + }, + }, +} satisfies StoryObj; diff --git a/packages/component-library/src/TableGrid/index.tsx b/packages/component-library/src/TableGrid/index.tsx new file mode 100644 index 0000000000..b0287d97a7 --- /dev/null +++ b/packages/component-library/src/TableGrid/index.tsx @@ -0,0 +1,119 @@ +import { + AllCommunityModule, + ClientSideRowModelModule, + ModuleRegistry, + TextFilterModule, + themeQuartz, +} from "ag-grid-community"; +import { AgGridReact } from "ag-grid-react"; +import type { ReactNode } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; + +import { Card } from "../Card"; +import { Paginator } from "../Paginator"; +import { Skeleton } from "../Skeleton"; +import styles from "./index.module.scss"; +import type { TableGridProps } from "./table-grid-props"; + +// Register all Community features +ModuleRegistry.registerModules([ + AllCommunityModule, + TextFilterModule, + ClientSideRowModelModule, +]); + +const SkeletonCellRenderer = (props: { value?: ReactNode }) => { + if (!props.value) { + return ( +
+
+ +
+
+ ); + } + return
{props.value}
; +}; + +const DEFAULT_COL_DEF = { + cellRenderer: SkeletonCellRenderer, + flex: 1, +}; + +export const TableGrid = >({ + rowData, + columnDefs, + loading, + cardProps, + pagination, + ...props +}: TableGridProps) => { + const gridRef = useRef>(null); + const [pageSize, setPageSize] = useState(10); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + + const mappedColDefs = useMemo(() => { + return columnDefs.map((colDef) => { + return { + ...colDef, + // the types in ag-grid are `any` for the cellRenderers which is throwing an error here + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + cellRenderer: loading + ? (colDef.loadingCellRenderer ?? SkeletonCellRenderer) + : colDef.cellRenderer, + }; + }); + }, [columnDefs, loading]); + + const onPaginationChanged = useCallback(() => { + const api = gridRef.current?.api; + if (!api) return; + setPageSize(api.paginationGetPageSize()); + setCurrentPage(api.paginationGetCurrentPage() + 1); + setTotalPages(api.paginationGetTotalPages()); + }, []); + + const onPageChange = useCallback((newPage: number) => { + gridRef.current?.api.paginationGoToPage(newPage - 1); + }, []); + + const tableGrid = ( + + className={styles.tableGrid} + // @ts-expect-error empty row data, which is throwing an error here btu required to display 1 row in the loading state + rowData={loading ? [[]] : rowData} + defaultColDef={DEFAULT_COL_DEF} + columnDefs={mappedColDefs} + theme={themeQuartz} + domLayout="autoHeight" + pagination={pagination ?? false} + paginationPageSize={pageSize} + suppressPaginationPanel + onPaginationChanged={onPaginationChanged} + ref={gridRef} + {...props} + /> + ); + if (!cardProps && !pagination) { + return tableGrid; + } + return ( + + ) + } + {...cardProps} + > + {tableGrid} + + ); +}; diff --git a/packages/component-library/src/TableGrid/table-grid-props.ts b/packages/component-library/src/TableGrid/table-grid-props.ts new file mode 100644 index 0000000000..b2af133e49 --- /dev/null +++ b/packages/component-library/src/TableGrid/table-grid-props.ts @@ -0,0 +1,17 @@ +import type { ColDef } from "ag-grid-community"; +import type { AgGridReactProps } from "ag-grid-react"; + +import type { Props as CardProps } from "../Card"; + +type ExtendedColDef = ColDef & { + loadingCellRenderer?: ColDef["cellRenderer"]; +}; + +export type TableGridProps> = { + rowData: TData[]; + columnDefs: ExtendedColDef[]; + cardProps?: Omit, "children" | "footer"> & { + nonInteractive?: true; + }; + pagination?: boolean; +} & Omit, "rowData" | "defaultColDef" | "columnDefs">; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e89786681..66e2d13862 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,6 +147,12 @@ catalogs: '@vercel/functions': specifier: ^2.0.0 version: 2.0.0 + ag-grid-community: + specifier: ^34.2.0 + version: 34.2.0 + ag-grid-react: + specifier: ^34.2.0 + version: 34.2.0 async-cache-dedupe: specifier: ^3.0.0 version: 3.0.0 @@ -2166,6 +2172,12 @@ importers: '@react-hookz/web': specifier: 'catalog:' version: 25.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + ag-grid-community: + specifier: 'catalog:' + version: 34.2.0 + ag-grid-react: + specifier: 'catalog:' + version: 34.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) bcp-47: specifier: 'catalog:' version: 2.1.0 @@ -12010,6 +12022,18 @@ packages: aes-js@4.0.0-beta.5: resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + ag-charts-types@12.2.0: + resolution: {integrity: sha512-d2qQrQirt9wP36YW5HPuOvXsiajyiFnr1CTsoCbs02bavPDz7Lk2jHp64+waM4YKgXb3GN7gafbBI9Qgk33BmQ==} + + ag-grid-community@34.2.0: + resolution: {integrity: sha512-peS7THEMYwpIrwLQHmkRxw/TlOnddD/F5A88RqlBxf8j+WqVYRWMOOhU5TqymGcha7z2oZ8IoL9ROl3gvtdEjg==} + + ag-grid-react@34.2.0: + resolution: {integrity: sha512-dLKFw6hz75S0HLuZvtcwjm+gyiI4gXVzHEu7lWNafWAX0mb8DhogEOP5wbzAlsN6iCfi7bK/cgZImZFjenlqwg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -38279,6 +38303,19 @@ snapshots: aes-js@4.0.0-beta.5: {} + ag-charts-types@12.2.0: {} + + ag-grid-community@34.2.0: + dependencies: + ag-charts-types: 12.2.0 + + ag-grid-react@34.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + ag-grid-community: 34.2.0 + prop-types: 15.8.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + agent-base@6.0.2: dependencies: debug: 4.4.0(supports-color@8.1.1) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ad63702af2..583fe85248 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -51,6 +51,8 @@ catalog: "@amplitude/analytics-browser": ^2.13.0 "@amplitude/plugin-autocapture-browser": ^1.0.0 "@axe-core/react": ^4.10.1 + "ag-grid-community": ^34.2.0 + "ag-grid-react": ^34.2.0 "@babel/cli": ^7.27.2 "@babel/core": ^7.27.1 "@babel/preset-typescript": ^7.27.1