Task/Orders data provider for libs/order-list (#173)

* add small/large options for loader

* switch order-list lib to use orders data provider

* prepare incoming orders for table insertion

* delete dupe file, fix test

* ignore sentry-cli from netlify build

* move sentry/nextjs to dev deps

* exclude sentry-cli from netlify
This commit is contained in:
Matthew Russell 2022-04-04 13:41:19 -07:00 committed by GitHub
parent 018e5fe139
commit 3b56dcd2b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 337 additions and 418 deletions

View File

@ -37,7 +37,7 @@ export const OrderDialog = ({
return ( return (
<OrderDialogWrapper <OrderDialogWrapper
title="Awaiting network confirmation" title="Awaiting network confirmation"
icon={<Loader />} icon={<Loader size="small" />}
> >
{transaction.hash && ( {transaction.hash && (
<p data-testid="tx-hash" className="break-all"> <p data-testid="tx-hash" className="break-all">

View File

@ -0,0 +1,4 @@
{
"name": "@vegaprotocol/order-list",
"version": "0.0.1"
}

View File

@ -4,6 +4,26 @@
"projectType": "library", "projectType": "library",
"tags": [], "tags": [],
"targets": { "targets": {
"build": {
"executor": "@nrwl/web:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/order-list",
"tsConfig": "libs/order-list/tsconfig.lib.json",
"project": "libs/order-list/package.json",
"entryFile": "libs/order-list/src/index.ts",
"external": ["react/jsx-runtime"],
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
"compiler": "babel",
"assets": [
{
"glob": "libs/order-list/README.md",
"input": ".",
"output": "."
}
]
}
},
"lint": { "lint": {
"executor": "@nrwl/linter:eslint", "executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"], "outputs": ["{options.outputFile}"],

View File

@ -1,2 +1,2 @@
export * from './order-list'; export * from './lib/order-list';
export * from './order-list-container'; export * from './lib/order-list-container';

View File

@ -1,17 +1,17 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { OrderListManager } from './order-list-manager'; import { OrderListManager } from './order-list-manager';
import * as useOrdersHook from './use-orders'; import * as useDataProviderHook from '@vegaprotocol/react-helpers';
import type { Orders_party_orders } from './__generated__/Orders'; import type { Orders_party_orders } from './__generated__/Orders';
import * as orderListMock from './order-list';
import { forwardRef } from 'react';
jest.mock('./order-list', () => ({ jest.mock('./order-list');
OrderList: () => <div>OrderList</div>,
}));
test('Renders a loading state while awaiting orders', () => { test('Renders a loading state while awaiting orders', () => {
jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({ jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
orders: [], data: [],
loading: true, loading: true,
error: null, error: undefined,
}); });
render(<OrderListManager partyId="0x123" />); render(<OrderListManager partyId="0x123" />);
expect(screen.getByText('Loading...')).toBeInTheDocument(); expect(screen.getByText('Loading...')).toBeInTheDocument();
@ -19,8 +19,8 @@ test('Renders a loading state while awaiting orders', () => {
test('Renders an error state', () => { test('Renders an error state', () => {
const errorMsg = 'Oops! An Error'; const errorMsg = 'Oops! An Error';
jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({ jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
orders: [], data: [],
loading: false, loading: false,
error: new Error(errorMsg), error: new Error(errorMsg),
}); });
@ -31,10 +31,13 @@ test('Renders an error state', () => {
}); });
test('Renders the order list if orders provided', async () => { test('Renders the order list if orders provided', async () => {
jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({ // @ts-ignore Orderlist is read only but we need to override with the forwardref to
orders: [{ id: '1' } as Orders_party_orders], // avoid warnings about padding refs
orderListMock.OrderList = forwardRef(() => <div>OrderList</div>);
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
data: [{ id: '1' } as Orders_party_orders],
loading: false, loading: false,
error: null, error: undefined,
}); });
render(<OrderListManager partyId="0x123" />); render(<OrderListManager partyId="0x123" />);
expect(await screen.findByText('OrderList')).toBeInTheDocument(); expect(await screen.findByText('OrderList')).toBeInTheDocument();

View File

@ -0,0 +1,79 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { OrderList } from './order-list';
import type { OrderFields } from './__generated__/OrderFields';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import {
ordersDataProvider,
prepareIncomingOrders,
sortOrders,
} from './orders-data-provider';
import { useCallback, useMemo, useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { OrderSub_orders } from './__generated__/OrderSub';
import isEqual from 'lodash/isEqual';
interface OrderListManagerProps {
partyId: string;
}
export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const variables = useMemo(() => ({ partyId }), [partyId]);
// Apply updates to the table
const update = useCallback((delta: OrderSub_orders[]) => {
if (!gridRef.current) {
return false;
}
const incoming = prepareIncomingOrders(delta);
const update: OrderFields[] = [];
const add: OrderFields[] = [];
incoming.forEach((d) => {
if (!gridRef.current) {
return;
}
const rowNode = gridRef.current.api.getRowNode(d.id);
if (rowNode) {
if (!isEqual) {
update.push(d);
}
} else {
add.push(d);
}
});
if (update.length || add.length) {
gridRef.current.api.applyTransactionAsync({
update,
add,
addIndex: 0,
});
}
return true;
}, []);
const { data, error, loading } = useDataProvider(
ordersDataProvider,
update,
variables
);
const orders = useMemo(() => {
if (!data) {
return null;
}
return sortOrders(data);
}, [data]);
return (
<AsyncRenderer loading={loading} error={error} data={orders}>
{(data) => <OrderList ref={gridRef} data={data} />}
</AsyncRenderer>
);
};

View File

@ -12,7 +12,7 @@ import { OrderList } from './order-list';
test('No orders message shown', async () => { test('No orders message shown', async () => {
await act(async () => { await act(async () => {
render(<OrderList orders={[]} />); render(<OrderList data={[]} />);
}); });
expect(screen.getByText('No orders')).toBeInTheDocument(); expect(screen.getByText('No orders')).toBeInTheDocument();
}); });
@ -77,7 +77,7 @@ const limitOrder: Orders_party_orders = {
test('Correct columns are rendered', async () => { test('Correct columns are rendered', async () => {
await act(async () => { await act(async () => {
render(<OrderList orders={[marketOrder]} />); render(<OrderList data={[marketOrder]} />);
}); });
const headers = screen.getAllByRole('columnheader'); const headers = screen.getAllByRole('columnheader');
@ -87,7 +87,7 @@ test('Correct columns are rendered', async () => {
'Amount', 'Amount',
'Type', 'Type',
'Status', 'Status',
'Remaining', 'Filled',
'Price', 'Price',
'Time In Force', 'Time In Force',
'Created At', 'Created At',
@ -96,7 +96,7 @@ test('Correct columns are rendered', async () => {
test('Correct formatting applied for market order', async () => { test('Correct formatting applied for market order', async () => {
await act(async () => { await act(async () => {
render(<OrderList orders={[marketOrder]} />); render(<OrderList data={[marketOrder]} />);
}); });
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
@ -117,7 +117,7 @@ test('Correct formatting applied for market order', async () => {
test('Correct formatting applied for GTT limit order', async () => { test('Correct formatting applied for GTT limit order', async () => {
await act(async () => { await act(async () => {
render(<OrderList orders={[limitOrder]} />); render(<OrderList data={[limitOrder]} />);
}); });
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
const expectedValues = [ const expectedValues = [
@ -144,7 +144,7 @@ test('Correct formatting applied for a rejected order', async () => {
rejectionReason: OrderRejectionReason.InsufficientAssetBalance, rejectionReason: OrderRejectionReason.InsufficientAssetBalance,
}; };
await act(async () => { await act(async () => {
render(<OrderList orders={[rejectedOrder]} />); render(<OrderList data={[rejectedOrder]} />);
}); });
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
expect(cells[3]).toHaveTextContent( expect(cells[3]).toHaveTextContent(

View File

@ -0,0 +1,89 @@
import { OrderTimeInForce, OrderStatus, Side } from '@vegaprotocol/types';
import type { Orders_party_orders } from './__generated__/Orders';
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import type { ValueFormatterParams } from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { forwardRef } from 'react';
interface OrderListProps {
data: Orders_party_orders[] | null;
}
export const OrderList = forwardRef<AgGridReact, OrderListProps>(
({ data }, ref) => {
return (
<AgGrid
ref={ref}
rowData={data}
overlayNoRowsTemplate="No orders"
defaultColDef={{ flex: 1, resizable: true }}
style={{ width: '100%', height: '100%' }}
getRowNodeId={(data) => data.id}
>
<AgGridColumn
headerName="Market"
field="market.tradableInstrument.instrument.code"
/>
<AgGridColumn
headerName="Amount"
field="size"
cellClass="font-mono"
valueFormatter={({ value, data }: ValueFormatterParams) => {
const prefix = data.side === Side.Buy ? '+' : '-';
return prefix + value;
}}
/>
<AgGridColumn field="type" />
<AgGridColumn
field="status"
valueFormatter={({ value, data }: ValueFormatterParams) => {
if (value === OrderStatus.Rejected) {
return `${value}: ${data.rejectionReason}`;
}
return value;
}}
/>
<AgGridColumn
headerName="Filled"
field="remaining"
cellClass="font-mono"
valueFormatter={({ data }: ValueFormatterParams) => {
return `${Number(data.size) - Number(data.remaining)}/${data.size}`;
}}
/>
<AgGridColumn
field="price"
cellClass="font-mono"
valueFormatter={({ value, data }: ValueFormatterParams) => {
if (data.type === 'Market') {
return '-';
}
return formatNumber(value, data.market.decimalPlaces);
}}
/>
<AgGridColumn
field="timeInForce"
valueFormatter={({ value, data }: ValueFormatterParams) => {
if (value === OrderTimeInForce.GTT && data.expiresAt) {
const expiry = getDateTimeFormat().format(
new Date(data.expiresAt)
);
return `${value}: ${expiry}`;
}
return value;
}}
/>
<AgGridColumn
field="createdAt"
valueFormatter={({ value }: ValueFormatterParams) => {
return getDateTimeFormat().format(new Date(value));
}}
/>
</AgGrid>
);
}
);

View File

@ -0,0 +1,104 @@
import { gql } from '@apollo/client';
import { makeDataProvider } from '@vegaprotocol/react-helpers';
import type { OrderFields } from './__generated__/OrderFields';
import type { Orders, Orders_party_orders } from './__generated__/Orders';
import type { OrderSub } from './__generated__/OrderSub';
import orderBy from 'lodash/orderBy';
import uniqBy from 'lodash/uniqBy';
const ORDER_FRAGMENT = gql`
fragment OrderFields on Order {
id
market {
id
name
decimalPlaces
tradableInstrument {
instrument {
code
}
}
}
type
side
size
status
rejectionReason
price
timeInForce
remaining
expiresAt
createdAt
updatedAt
}
`;
export const ORDERS_QUERY = gql`
${ORDER_FRAGMENT}
query Orders($partyId: ID!) {
party(id: $partyId) {
id
orders {
...OrderFields
}
}
}
`;
export const ORDERS_SUB = gql`
${ORDER_FRAGMENT}
subscription OrderSub($partyId: ID!) {
orders(partyId: $partyId) {
...OrderFields
}
}
`;
// A single update can contain the same order with multiple updates, so we need to find
// the latest version of the order and only update using that
export const sortOrders = (orders: OrderFields[]) => {
return orderBy(
orders,
(o) => {
if (!o.updatedAt) return new Date(o.createdAt).getTime();
return new Date(o.updatedAt).getTime();
},
'desc'
);
};
export const uniqOrders = (orders: OrderFields[]) => {
return uniqBy(orders, 'id');
};
export const prepareIncomingOrders = (delta: OrderFields[]) => {
const sortedOrders = sortOrders(delta);
const incoming = uniqOrders(sortedOrders);
return incoming;
};
const update = (draft: OrderFields[], delta: OrderFields[]) => {
const incoming = prepareIncomingOrders(delta);
// Add or update incoming orders
incoming.forEach((order) => {
const index = draft.findIndex((o) => o.id === order.id);
if (index === -1) {
draft.unshift(order);
} else {
draft[index] = order;
}
});
};
const getData = (responseData: Orders): Orders_party_orders[] | null =>
responseData?.party?.orders || null;
const getDelta = (subscriptionData: OrderSub) => subscriptionData.orders || [];
export const ordersDataProvider = makeDataProvider(
ORDERS_QUERY,
ORDERS_SUB,
update,
getData,
getDelta
);

View File

@ -1,18 +0,0 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useOrders } from './use-orders';
import { OrderList } from './order-list';
import type { OrderFields } from './__generated__/OrderFields';
interface OrderListManagerProps {
partyId: string;
}
export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
const { orders, loading, error } = useOrders(partyId);
return (
<AsyncRenderer<OrderFields[]> loading={loading} error={error} data={orders}>
{(data) => <OrderList orders={data} />}
</AsyncRenderer>
);
};

View File

@ -1,96 +0,0 @@
import { OrderTimeInForce, OrderStatus, Side } from '@vegaprotocol/types';
import type { Orders_party_orders } from './__generated__/Orders';
import {
formatNumber,
getDateTimeFormat,
t,
useApplyGridTransaction,
} from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import type { GridApi, ValueFormatterParams } from 'ag-grid-community';
import { AgGridColumn } from 'ag-grid-react';
import { useRef, useState } from 'react';
interface OrderListProps {
orders: Orders_party_orders[];
}
export const OrderList = ({ orders }: OrderListProps) => {
// Store initial orders for initial table data set, further updates
// are handled by the effect below
const [initialOrders] = useState(orders);
const gridApi = useRef<GridApi | null>(null);
useApplyGridTransaction<Orders_party_orders>(orders, gridApi.current);
return (
<AgGrid
rowData={initialOrders}
overlayNoRowsTemplate="No orders"
defaultColDef={{ flex: 1, resizable: true }}
style={{ width: '100%', height: '100%' }}
onGridReady={(params) => {
gridApi.current = params.api;
}}
getRowNodeId={(data) => data.id}
>
<AgGridColumn
headerName={t('Market')}
field="market.tradableInstrument.instrument.code"
/>
<AgGridColumn
headerName={t('Amount')}
field="size"
cellClass="font-mono"
valueFormatter={({ value, data }: ValueFormatterParams) => {
const prefix = data.side === Side.Buy ? '+' : '-';
return prefix + value;
}}
/>
<AgGridColumn field="type" />
<AgGridColumn
field="status"
valueFormatter={({ value, data }: ValueFormatterParams) => {
if (value === OrderStatus.Rejected) {
return `${value}: ${data.rejectionReason}`;
}
return value;
}}
/>
<AgGridColumn
field="remaining"
cellClass="font-mono"
valueFormatter={({ data }: ValueFormatterParams) => {
return `${Number(data.size) - Number(data.remaining)}/${data.size}`;
}}
/>
<AgGridColumn
field="price"
cellClass="font-mono"
valueFormatter={({ value, data }: ValueFormatterParams) => {
if (data.type === 'Market') {
return '-';
}
return formatNumber(value, data.market.decimalPlaces);
}}
/>
<AgGridColumn
field="timeInForce"
valueFormatter={({ value, data }: ValueFormatterParams) => {
if (value === OrderTimeInForce.GTT && data.expiresAt) {
const expiry = getDateTimeFormat().format(new Date(data.expiresAt));
return `${value}: ${expiry}`;
}
return value;
}}
/>
<AgGridColumn
field="createdAt"
valueFormatter={({ value }: ValueFormatterParams) => {
return getDateTimeFormat().format(new Date(value));
}}
/>
</AgGrid>
);
};

View File

@ -1,138 +0,0 @@
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react-hooks';
import type { OrderFields } from './__generated__/OrderFields';
import type { Orders } from './__generated__/Orders';
import type { OrderSub } from './__generated__/OrderSub';
import {
OrderStatus,
Side,
OrderType,
OrderTimeInForce,
} from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import { ORDERS_QUERY, ORDERS_SUB, useOrders } from './use-orders';
const partyId = '0x123';
function generateOrder(order?: Partial<OrderFields>): OrderFields {
return {
__typename: 'Order',
id: '1',
market: {
__typename: 'Market',
id: 'market-id',
name: 'market-name',
decimalPlaces: 0,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
code: 'instrument-code',
},
},
},
type: OrderType.Market,
side: Side.Buy,
size: '10',
status: OrderStatus.Active,
rejectionReason: null,
price: '',
timeInForce: OrderTimeInForce.GTC,
remaining: '10',
createdAt: '2022-01-01T00:00:00',
updatedAt: null,
expiresAt: null,
...order,
};
}
function setup(mocks: MockedResponse[] = [], id: string | null) {
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks}>{children}</MockedProvider>
);
return renderHook(() => useOrders(id as string), { wrapper });
}
test('Fetches and subscribes to orders and merges appropriately', async () => {
const order = generateOrder();
const mockOrderQuery: MockedResponse<Orders> = {
request: {
query: ORDERS_QUERY,
variables: { partyId },
},
result: {
data: {
party: {
__typename: 'Party',
id: partyId,
orders: [order],
},
},
},
};
const updatedOrder = generateOrder({
id: '1',
remaining: '5',
updatedAt: '2022-01-01T00:01:00',
});
const newOrder = generateOrder({
id: '2',
createdAt: '2022-01-01T01:00:00',
});
const mockOrderSub: MockedResponse<OrderSub> = {
request: {
query: ORDERS_SUB,
variables: { partyId },
},
result: {
data: {
orders: [updatedOrder, newOrder],
},
},
delay: 100,
};
const { result, waitForNextUpdate } = setup(
[mockOrderQuery, mockOrderSub],
partyId
);
expect(result.current.loading).toBe(true);
expect(result.current.error).toBe(null);
await waitForNextUpdate();
expect(result.current.orders).toEqual([order]);
expect(result.current.loading).toBe(false);
await waitForNextUpdate();
expect(result.current.orders).toEqual([newOrder, updatedOrder]);
});
test('Returns an error if fetch fails', async () => {
const error = new Error('Something failed');
const mockFailedOrderQuery: MockedResponse<Orders> = {
request: {
query: ORDERS_QUERY,
variables: { partyId },
},
error,
};
const { result, waitForNextUpdate } = setup([mockFailedOrderQuery], partyId);
expect(result.current.loading).toBe(true);
expect(result.current.error).toBe(null);
await waitForNextUpdate();
expect(result.current.error).toEqual(error);
expect(result.current.loading).toBe(false);
});
test('No queries are made if no pubkey provided', () => {
const mockQuery: MockedResponse<Orders> = {
request: {
query: ORDERS_QUERY,
variables: { partyId },
},
newData: jest.fn(),
};
const { result } = setup([mockQuery], null);
expect(mockQuery.newData).not.toBeCalled();
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(null);
});

View File

@ -1,138 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { gql, useApolloClient } from '@apollo/client';
import uniqBy from 'lodash/uniqBy';
import orderBy from 'lodash/orderBy';
import type { Orders, OrdersVariables } from './__generated__/Orders';
import type { OrderSub, OrderSubVariables } from './__generated__/OrderSub';
import type { OrderFields } from './__generated__/OrderFields';
const ORDER_FRAGMENT = gql`
fragment OrderFields on Order {
id
market {
id
name
decimalPlaces
tradableInstrument {
instrument {
code
}
}
}
type
side
size
status
rejectionReason
price
timeInForce
remaining
expiresAt
createdAt
updatedAt
}
`;
export const ORDERS_QUERY = gql`
${ORDER_FRAGMENT}
query Orders($partyId: ID!) {
party(id: $partyId) {
id
orders {
...OrderFields
}
}
}
`;
export const ORDERS_SUB = gql`
${ORDER_FRAGMENT}
subscription OrderSub($partyId: ID!) {
orders(partyId: $partyId) {
...OrderFields
}
}
`;
interface UseOrders {
orders: OrderFields[];
error: Error | null;
loading: boolean;
}
export const useOrders = (partyId: string): UseOrders => {
const client = useApolloClient();
const [orders, setOrders] = useState<OrderFields[]>([]);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const mergeOrders = useCallback((update: OrderFields[]) => {
// A subscription payload can contain multiple updates for a single order so we need to first
// sort them by updatedAt (or createdAt if the order hasn't been updated) with the newest first,
// then use uniqBy, which selects the first occuring order for an id to ensure we only get the latest order
setOrders((curr) => {
const sorted = orderBy(
[...curr, ...update],
(o) => {
if (!o.updatedAt) return new Date(o.createdAt).getTime();
return new Date(o.updatedAt).getTime();
},
'desc'
);
return uniqBy(sorted, 'id');
});
}, []);
// Make initial fetch
useEffect(() => {
const fetchOrders = async () => {
if (!partyId) return;
setLoading(true);
try {
const res = await client.query<Orders, OrdersVariables>({
query: ORDERS_QUERY,
variables: { partyId },
});
if (!res.data.party?.orders?.length) return;
mergeOrders(res.data.party.orders);
} catch (err) {
setError(
err instanceof Error ? err : new Error('Something went wrong')
);
} finally {
setLoading(false);
}
};
fetchOrders();
}, [mergeOrders, partyId, client]);
// Start subscription
useEffect(() => {
if (!partyId) return;
const sub = client
.subscribe<OrderSub, OrderSubVariables>({
query: ORDERS_SUB,
variables: { partyId },
})
.subscribe(({ data }) => {
if (!data?.orders) {
return;
}
mergeOrders(data.orders);
});
return () => {
if (sub) {
sub.unsubscribe();
}
};
}, [client, partyId, mergeOrders]);
return { orders, error, loading };
};

View File

@ -8,8 +8,7 @@ interface AsyncRendererProps<T> {
children: (data: T) => ReactNode; children: (data: T) => ReactNode;
} }
// eslint-disable-next-line export function AsyncRenderer<T = object>({
export function AsyncRenderer<T = any>({
loading, loading,
error, error,
data, data,

View File

@ -1,6 +1,10 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
export const Loader = () => { interface LoaderProps {
size?: 'small' | 'large';
}
export const Loader = ({ size = 'large' }: LoaderProps) => {
const [, forceRender] = useState(false); const [, forceRender] = useState(false);
useEffect(() => { useEffect(() => {
@ -11,13 +15,17 @@ export const Loader = () => {
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
const wrapperClasses = size === 'small' ? 'w-[15px] h-[15px]' : 'w-64 h-64';
const gridItemClasses = size === 'small' ? 'w-[5px] h-[5px]' : 'w-16 h-16';
const items = size === 'small' ? 9 : 16;
return ( return (
<div className="flex flex-col items-center" data-testid="splash-loader"> <div className="flex flex-col items-center" data-testid="splash-loader">
<div className="w-64 h-64 flex flex-wrap"> <div className={`${wrapperClasses} flex flex-wrap`}>
{new Array(16).fill(null).map((_, i) => { {new Array(items).fill(null).map((_, i) => {
return ( return (
<div <div
className="w-16 h-16 dark:bg-white bg-black" className={`${gridItemClasses} dark:bg-white bg-black`}
key={i} key={i}
style={{ style={{
opacity: Math.random() > 0.75 ? 1 : 0, opacity: Math.random() > 0.75 ? 1 : 0,

View File

@ -1,2 +1,5 @@
[build] [build]
ignore = "node ignore-netlify-build.js" ignore = "node ignore-netlify-build.js"
[functions]
included_files = ["!node_modules/@sentry/cli/sentry-cli"]