diff --git a/packages/react/README.md b/packages/react/README.md index 4366c579..ecf85840 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -112,7 +112,7 @@ function App() { [SignUpForm](./src/components/SignUpForm/README.md) -[InstanceTable](./src/components/InstanceTable/README.md) +[InstanceList](./src/components/InstanceList/README.md) [Metrics](./src/components/Metrics/README.md) diff --git a/packages/react/src/components/InstanceTable/InstanceTable.tsx b/packages/react/src/components/InstanceList/InstanceList.tsx similarity index 91% rename from packages/react/src/components/InstanceTable/InstanceTable.tsx rename to packages/react/src/components/InstanceList/InstanceList.tsx index bb34af97..e68c6588 100644 --- a/packages/react/src/components/InstanceTable/InstanceTable.tsx +++ b/packages/react/src/components/InstanceList/InstanceList.tsx @@ -6,9 +6,9 @@ import { Entity, Instance, Organization } from '@theniledev/js'; import { TableWrapper, TableSkeleton } from '../../lib/table'; import { flattenSchema, flatten } from '../../lib/utils/schema'; -import { InstanceTableProps } from './types'; +import { InstanceListProps } from './types'; -type Props = Omit & { +type Props = Omit & { instances: void | Instance[]; entityData: void | Entity; organization: void | Organization; @@ -22,7 +22,7 @@ export const generateHeaderRow = ( entityData: void | Entity, // eslint-disable-next-line @typescript-eslint/no-explicit-any processColumns?: (header: string, flatSchema: any) => GridColDef -) => { +): GridColDef[] => { const flatSchema = entityData && flattenSchema(entityData?.schema, true); if (flatSchema) { const baseArr = Object.keys(flatSchema).map((header) => { @@ -47,9 +47,12 @@ export const generateHeaderRow = ( {} ); - return columns.map((col) => { - return colLookup[String(col)]; - }); + return columns.reduce((accum: GridColDef[], col) => { + if (colLookup[String(col)] != null) { + accum.push(colLookup[String(col)]); + } + return accum; + }, []); } if (additionalColumns && additionalColumns.length) { return baseArr.concat(additionalColumns); @@ -72,7 +75,7 @@ export const generateHeaderRow = ( return []; }; -const InstanceTable = React.memo(function InstanceTable(props: Props) { +const InstanceList = React.memo(function InstanceList(props: Props) { const { isFetching, instances, @@ -183,4 +186,4 @@ const InstanceTable = React.memo(function InstanceTable(props: Props) { ); }); -export default InstanceTable; +export default InstanceList; diff --git a/packages/react/src/components/InstanceTable/InstanceTableDataFetcher.tsx b/packages/react/src/components/InstanceList/InstanceListDataFetcher.tsx similarity index 64% rename from packages/react/src/components/InstanceTable/InstanceTableDataFetcher.tsx rename to packages/react/src/components/InstanceList/InstanceListDataFetcher.tsx index 5b392fd6..360f396f 100644 --- a/packages/react/src/components/InstanceTable/InstanceTableDataFetcher.tsx +++ b/packages/react/src/components/InstanceList/InstanceListDataFetcher.tsx @@ -1,14 +1,19 @@ import React from 'react'; +import { useInterval } from '../../lib/hooks/useInterval'; import { useNile } from '../../context'; import Queries, { useQuery } from '../../lib/queries'; -import InstanceTable from './InstanceTable'; -import { InstanceTableProps } from './types'; +import InstanceList from './InstanceList'; +import { InstanceListProps, ComponentProps } from './types'; -export type InstanceTableDataFetcherProps = InstanceTableProps; -export default function InstanceTableDataFetcher( - props: InstanceTableDataFetcherProps +export type InstanceListDataFetcherProps = InstanceListProps & { + refreshInterval?: number; + Component: ComponentProps; +}; + +export default function InstanceListDataFetcher( + props: InstanceListDataFetcherProps ) { const { entity, @@ -22,6 +27,8 @@ export default function InstanceTableDataFetcher( useQuery: customUseQuery, processColumns, actionButtons, + refreshInterval, + Component = InstanceList, } = props; const nile = useNile(); const useQueryHook = customUseQuery ?? useQuery; @@ -36,16 +43,21 @@ export default function InstanceTableDataFetcher( () => nile.entities.getEntity({ type: String(entity) }) ); - const { data: instances, isFetching: isInstancesFetching } = useQueryHook( - Queries.ListInstances(entity, org), - () => - nile.entities.listInstances({ - type: String(entity), - org: String(org), - }) + const { + refetch, + data: instances, + isFetching: isInstancesFetching, + } = useQueryHook(Queries.ListInstances(entity, org), () => + nile.entities.listInstances({ + type: String(entity), + org: String(org), + }) ); + + useInterval(refetch, refreshInterval); + return ( - + + + ); +} +``` + +## Customization + +See the [InstanceList storybook](https://react-storybook-ten.vercel.app/?path=/story/InstanceList--default) for samples on customizing `` to fit your needs. + +If you need full control over rendering, you can pass a `Component` prop. `` will do the data fetching, and pass props back to you. + +```typescript +import { InstanceList, NileProvider, ComponentProps } from '@theniledev/react'; + +const API_URL = 'http://localhost:8080'; // location of the Nile endpoint + +function HandleEverything(props: ComponentProps) { + return <>{JSON.stringify(props)}; +} + +function App() { + return ( + + + + ); +} +``` + +## Theming + +[theming](../../../README.md#UI%20customization) diff --git a/packages/react/src/components/InstanceList/index.tsx b/packages/react/src/components/InstanceList/index.tsx new file mode 100644 index 00000000..c7014b2f --- /dev/null +++ b/packages/react/src/components/InstanceList/index.tsx @@ -0,0 +1,3 @@ +import { default as InstanceList } from './InstanceListDataFetcher'; + +export default InstanceList; diff --git a/packages/react/src/components/InstanceTable/types.ts b/packages/react/src/components/InstanceList/types.ts similarity index 84% rename from packages/react/src/components/InstanceTable/types.ts rename to packages/react/src/components/InstanceList/types.ts index 68ab01a3..30b0cb3e 100644 --- a/packages/react/src/components/InstanceTable/types.ts +++ b/packages/react/src/components/InstanceList/types.ts @@ -2,7 +2,7 @@ import { GridRowParams, GridColDef } from '@mui/x-data-grid'; import { useQuery } from '@tanstack/react-query'; import { Instance, Organization } from '@theniledev/js'; -export interface InstanceTableProps { +export interface InstanceListProps { entity: string; org: string; handleRowClick?: (params: GridRowParams) => void; @@ -22,3 +22,7 @@ export interface InstanceTableProps { organization: Organization; }) => React.ReactNode; } + +export type ComponentProps = React.FunctionComponent< + Omit +>; diff --git a/packages/react/src/components/InstanceTable/README.md b/packages/react/src/components/InstanceTable/README.md deleted file mode 100644 index 6b37189f..00000000 --- a/packages/react/src/components/InstanceTable/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Instance Table - -A table (based on [data grid table in mui](https://mui.com/x/react-data-grid/)) for rendering instances. - -The default exports transparently handles request and loading state. If you need finer grained control, you could `InstanceTable` directly, but it may make more sense to implement your own wrapper around the react-data-grid table. - -## Usage - -```typescript -import { InstanceTable, NileProvider } from '@theniledev/react'; - -const API_URL = 'http://localhost:8080'; // location of the Nile endpoint - -function App() { - return ( - - - - ); -} -``` - -## Customization - -See the [InstanceTable storybook](https://react-storybook-ten.vercel.app/?path=/story/instancetable--default) for samples on customizing `` to fit your needs. - -## Theming - -[theming](../../../README.md#UI%20customization) diff --git a/packages/react/src/components/InstanceTable/index.tsx b/packages/react/src/components/InstanceTable/index.tsx deleted file mode 100644 index 9729330b..00000000 --- a/packages/react/src/components/InstanceTable/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { default as InstanceTable } from './InstanceTableDataFetcher'; - -export default InstanceTable; diff --git a/packages/react/src/components/Metrics/hooks.ts b/packages/react/src/components/Metrics/hooks.ts index e63cb0c0..0e157db1 100644 --- a/packages/react/src/components/Metrics/hooks.ts +++ b/packages/react/src/components/Metrics/hooks.ts @@ -8,6 +8,7 @@ import { import { useNile } from '../../context'; import Queries, { useQuery } from '../../lib/queries'; +import { useInterval } from '../../lib/hooks/useInterval'; type UseMetricsReturn = { isLoading: boolean; @@ -76,19 +77,3 @@ export const useMetrics = ( return { isLoading, metrics: flatMetrics }; }; - -export const useInterval = (cb: () => void, delay: void | number) => { - const savedCallback = React.useRef(cb); - - React.useEffect(() => { - savedCallback.current = cb; - }, [cb]); - - React.useEffect(() => { - if (!delay) { - return; - } - const id = setInterval(() => savedCallback.current(), delay); - return () => clearInterval(id); - }, [delay]); -}; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 555a4d24..e9551501 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,6 +1,6 @@ export { default as LoginForm } from './components/LoginForm'; export { default as SignUpForm } from './components/SignUpForm'; -export { default as InstanceTable } from './components/InstanceTable'; +export { default as InstanceList } from './components/InstanceList'; export { default as EntityForm } from './components/EntityForm'; export { NileProvider, useNile } from './context'; export { diff --git a/packages/react/src/lib/hooks/useInterval/index.ts b/packages/react/src/lib/hooks/useInterval/index.ts new file mode 100644 index 00000000..a4a41f6a --- /dev/null +++ b/packages/react/src/lib/hooks/useInterval/index.ts @@ -0,0 +1,17 @@ +import React from 'react'; + +export const useInterval = (cb: () => void, delay: void | number) => { + const savedCallback = React.useRef(cb); + + React.useEffect(() => { + savedCallback.current = cb; + }, [cb]); + + React.useEffect(() => { + if (!delay) { + return; + } + const id = setInterval(() => savedCallback.current(), delay); + return () => clearInterval(id); + }, [delay]); +}; diff --git a/packages/react/stories/InstanceTable.stories.tsx b/packages/react/stories/InstanceList.stories.tsx similarity index 96% rename from packages/react/stories/InstanceTable.stories.tsx rename to packages/react/stories/InstanceList.stories.tsx index e7a46043..66512564 100644 --- a/packages/react/stories/InstanceTable.stories.tsx +++ b/packages/react/stories/InstanceList.stories.tsx @@ -11,7 +11,7 @@ import { GridRenderCellParams } from '@mui/x-data-grid'; import { Add } from '@mui/icons-material'; import Card from '@mui/joy/Card'; -import InstanceTable from '../src/components/InstanceTable/InstanceTable'; +import InstanceList from '../src/components/InstanceList/InstanceList'; import { NileProvider } from '../src/context'; const entityData = { @@ -61,7 +61,7 @@ const instances: Instance[] = [ }, ]; const meta: Meta = { - component: InstanceTable, + component: InstanceList, argTypes: { isFetching: { description: @@ -93,7 +93,7 @@ type StoryProps = { const Template: Story = (args) => { return ( - ( - ) => { const Columns = () => ( - ( - ( - { let entityData: any; @@ -43,4 +43,13 @@ describe('instance table', () => { { field: 'familyName', flex: 1, headerName: 'familyName', minWidth: 200 }, ]); }); + + it('handles columns that do not exist', () => { + const additionalColumns: any = []; + const columns: any = ['not here', 'familyName']; + const headerRow = generateHeaderRow(additionalColumns, columns, entityData); + expect(headerRow).toEqual([ + { field: 'familyName', flex: 1, headerName: 'familyName', minWidth: 200 }, + ]); + }); });