{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table-remote",
  "title": "Data Table Remote",
  "description": "A headless data table that renders from an API endpoint: point it at a table manifest and a list endpoint and it derives columns, filters, sheet fields, row identity and capability-aware degradation with no per-column code.",
  "dependencies": [
    "@tanstack/react-query@^5.101.4",
    "@tanstack/react-table@^9.2.3",
    "nuqs@^2.3.0"
  ],
  "registryDependencies": [
    "https://data-table.openstatus.dev/r/data-table.json",
    "https://data-table.openstatus.dev/r/data-table-schema.json",
    "https://data-table.openstatus.dev/r/data-table-query.json",
    "https://data-table.openstatus.dev/r/data-table-nuqs.json"
  ],
  "files": [
    {
      "path": "src/components/data-table/data-table-remote/index.ts",
      "content": "export {\n  DataTableRemote,\n  type DataTableRemoteProps,\n  type RemoteSlots,\n} from \"./data-table-remote\";\nexport { defaultDataEndpoint, schemaIdentity } from \"./data-table-remote\";\n",
      "type": "registry:component"
    },
    {
      "path": "src/components/data-table/data-table-remote/data-table-remote.tsx",
      "content": "\"use client\";\n\nimport { DataTableInfinite } from \"@/components/data-table/data-table-infinite\";\nimport type {\n  DataTableFilterField,\n  SheetField,\n} from \"@/components/data-table/types\";\nimport { useTableManifest } from \"@/hooks/use-table-manifest\";\nimport type { UseTableManifestOptions } from \"@/hooks/use-table-manifest\";\nimport {\n  applyFacets,\n  createDataTableQueryOptions,\n  getFacetedMinMaxValues,\n  getFacetedUniqueValues,\n  getMetaPage,\n  type PaginationStrategy,\n  type Transport,\n} from \"@/lib/data-table\";\nimport { useNuqsAdapter } from \"@/lib/store/adapters/nuqs\";\nimport { useFilterState } from \"@/lib/store/hooks/useFilterState\";\nimport { DataTableStoreProvider } from \"@/lib/store/provider/DataTableStoreProvider\";\nimport { field } from \"@/lib/store/schema\";\nimport {\n  applyRenderers,\n  createRowAccessors,\n  createTableSchema,\n  generateColumns,\n  generateFilterFields,\n  generateFilterSchema,\n  generateSheetFields,\n  getDefaultColumnVisibility,\n  type RendererOverrides,\n  type TableChartConfig,\n  type TableManifest,\n} from \"@/lib/table-schema\";\nimport { useInfiniteQuery } from \"@tanstack/react-query\";\nimport * as React from \"react\";\n\n/**\n * A data table that renders from an endpoint rather than from local code.\n *\n * Point it at a manifest URL and a list URL and it draws the columns, the\n * filters, the sheet, the counts and the row actions — none of which it knows\n * anything about at build time.\n *\n * ## Why two components\n *\n * The URL-state adapter's parsers are derived from the schema, and\n * `useNuqsAdapter(schema, …)` needs one at first render. A hook cannot be\n * called conditionally, so the manifest has to resolve one level above\n * everything built from it. `DataTableRemote` resolves it; `RemoteTable` is\n * mounted only once it has, and so has a stable hook order for its whole life.\n *\n * Remounting on a schema change is deliberate rather than incidental: table\n * state (sorting, visibility, filters) is keyed by column, and carrying it\n * across a schema swap resurrects state for columns that no longer exist.\n */\n\ntype RemoteRow = Record<string, unknown>;\n\nexport type DataTableRemoteProps = {\n  /** `GET`s a `TableManifest`. See `createTableManifestHandler`. */\n  manifestEndpoint: string;\n  /** `GET`s rows. Defaults to `manifestEndpoint` with `/schema` stripped. */\n  dataEndpoint?: string;\n  /** Base URL, headers, credentials, response parsing. */\n  transport?: Transport<RemoteRow[], unknown>;\n  /** Defaults to a bidirectional timestamp cursor. */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  pagination?: PaginationStrategy<any>;\n  /** Serializes filter state into a query string for the data endpoint. */\n  searchParamsSerializer: (search: Record<string, unknown>) => string;\n  /** A build-time snapshot or a server prefetch — skips the manifest round trip. */\n  initialManifest?: TableManifest;\n  manifestOptions?: Omit<UseTableManifestOptions, \"initialManifest\">;\n  /**\n   * Renderer closures to attach by column key.\n   *\n   * A schema crossing the wire carries named displays (`badge`, `bar`,\n   * `status-code`, …) but not closures, so this is how an app draws the one or\n   * two columns the named set does not cover while every other column stays\n   * declarative. See `applyRenderers`.\n   */\n  renderers?: RendererOverrides;\n  /** Distinguishes this table's URL state and query cache from another's. */\n  tableId?: string;\n  /** Rendered while the manifest is in flight. */\n  loadingSlot?: React.ReactNode;\n  /** Rendered when the manifest could not be loaded. */\n  errorSlot?: (error: Error) => React.ReactNode;\n  className?: string;\n} & RemoteSlots;\n\n/** Host-supplied pieces the manifest cannot describe. */\nexport type RemoteSlots = {\n  commandSlot?: React.ReactNode;\n  toolbarActions?: React.ReactNode;\n  footerSlot?: React.ReactNode;\n  floatingBarSlot?: React.ReactNode;\n  /**\n   * Given the chart points and the manifest's chart config, when the endpoint\n   * says it has a chart. Not rendered at all when it does not.\n   */\n  chartSlot?: (\n    chartData: unknown[],\n    config: TableChartConfig | undefined,\n  ) => React.ReactNode;\n  sheetSlot?: (fields: SheetField<RemoteRow>[]) => React.ReactNode;\n  /** Per-row styling, which no manifest field describes. */\n  getRowClassName?: Parameters<\n    typeof DataTableInfinite<RemoteRow>\n  >[0][\"getRowClassName\"];\n};\n\n/** `/api/logs/schema` → `/api/logs`; anything else is returned unchanged. */\nexport function defaultDataEndpoint(manifestEndpoint: string): string {\n  return manifestEndpoint.replace(/\\/schema\\/?$/, \"\") || \"/\";\n}\n\nexport function DataTableRemote({\n  manifestEndpoint,\n  initialManifest,\n  manifestOptions,\n  loadingSlot,\n  errorSlot,\n  ...props\n}: DataTableRemoteProps) {\n  const query = useTableManifest(manifestEndpoint, {\n    ...manifestOptions,\n    ...(initialManifest ? { initialManifest } : {}),\n  });\n\n  if (query.error) {\n    return <>{errorSlot?.(query.error) ?? null}</>;\n  }\n  if (!query.data) {\n    return <>{loadingSlot ?? null}</>;\n  }\n\n  return (\n    <RemoteTable\n      // A schema change resets table state rather than carrying stale\n      // per-column state across it.\n      key={schemaIdentity(query.data)}\n      manifest={query.data}\n      manifestEndpoint={manifestEndpoint}\n      {...props}\n    />\n  );\n}\n\n/**\n * A cheap identity for \"is this the same schema\".\n *\n * Deliberately not the HTTP ETag: that covers the whole manifest, including\n * capability and action changes, and remounting on those would throw away the\n * user's sorting for no reason. Only the column set can strand table state.\n */\nexport function schemaIdentity(manifest: TableManifest): string {\n  return manifest.schema.columns.map((column) => column.key).join(\"|\");\n}\n\ntype RemoteTableProps = Omit<\n  DataTableRemoteProps,\n  \"initialManifest\" | \"manifestOptions\" | \"loadingSlot\" | \"errorSlot\"\n> & { manifest: TableManifest };\n\nfunction RemoteTable({\n  manifest,\n  manifestEndpoint,\n  dataEndpoint,\n  transport,\n  pagination,\n  searchParamsSerializer,\n  renderers,\n  tableId = \"remote\",\n  ...slots\n}: RemoteTableProps) {\n  const definition = React.useMemo(() => {\n    const { definition: base } = createTableSchema.fromJSON(manifest.schema);\n    return renderers ? applyRenderers(base, renderers) : base;\n  }, [manifest.schema, renderers]);\n\n  const columns = React.useMemo(\n    () => generateColumns<RemoteRow>(definition),\n    [definition],\n  );\n  const filterFields = React.useMemo(\n    () => generateFilterFields<RemoteRow>(definition),\n    [definition],\n  );\n  const sheetFields = React.useMemo(\n    () => generateSheetFields<RemoteRow>(definition),\n    [definition],\n  );\n  const defaultColumnVisibility = React.useMemo(\n    () => ({\n      ...getDefaultColumnVisibility(definition),\n      ...manifest.defaults?.columnVisibility,\n    }),\n    [definition, manifest.defaults?.columnVisibility],\n  );\n\n  // `sort` is spliced in before the schema is created so `defaults` cannot\n  // disagree with `definition` — the same ordering the builder needs.\n  const filterSchema = React.useMemo(\n    () => generateFilterSchema(definition, { sort: field.sort() }),\n    [definition],\n  );\n\n  const adapter = useNuqsAdapter(filterSchema.definition, { id: tableId });\n\n  const queryOptions = React.useMemo(\n    () =>\n      createDataTableQueryOptions<RemoteRow[], unknown>({\n        queryKeyPrefix: tableId,\n        apiEndpoint: dataEndpoint ?? defaultDataEndpoint(manifestEndpoint),\n        searchParamsSerializer,\n        ...(transport ? { transport } : {}),\n        ...(pagination ? { pagination } : {}),\n      }),\n    [\n      tableId,\n      dataEndpoint,\n      manifestEndpoint,\n      searchParamsSerializer,\n      transport,\n      pagination,\n    ],\n  );\n\n  return (\n    <DataTableStoreProvider adapter={adapter}>\n      <RemoteTableQuery\n        manifest={manifest}\n        columns={columns}\n        filterFields={filterFields}\n        sheetFields={sheetFields}\n        defaultColumnVisibility={defaultColumnVisibility}\n        queryOptions={queryOptions}\n        tableId={tableId}\n        {...slots}\n      />\n    </DataTableStoreProvider>\n  );\n}\n\ntype RemoteTableQueryProps = RemoteSlots & {\n  manifest: TableManifest;\n  columns: ReturnType<typeof generateColumns<RemoteRow>>;\n  filterFields: DataTableFilterField<RemoteRow>[];\n  sheetFields: SheetField<RemoteRow>[];\n  defaultColumnVisibility: Record<string, boolean>;\n  queryOptions: ReturnType<\n    typeof createDataTableQueryOptions<RemoteRow[], unknown>\n  >;\n  tableId: string;\n  className?: string;\n};\n\nfunction RemoteTableQuery({\n  manifest,\n  columns,\n  filterFields,\n  sheetFields,\n  defaultColumnVisibility,\n  queryOptions,\n  tableId,\n  chartSlot,\n  sheetSlot,\n  getRowClassName,\n  ...slots\n}: RemoteTableQueryProps) {\n  const search = useFilterState<Record<string, unknown>>();\n  const { capabilities } = manifest;\n\n  const {\n    data,\n    isFetching,\n    isLoading,\n    fetchNextPage,\n    hasNextPage,\n    fetchPreviousPage,\n    refetch,\n  } = useInfiniteQuery(queryOptions(search));\n\n  const flatData = React.useMemo(\n    () => data?.pages?.flatMap((page) => page.data ?? []) ?? [],\n    [data?.pages],\n  );\n\n  // Not the last page: with `backwardPagination`, live mode prepends via\n  // `fetchPreviousPage`, so the page carrying meta is not at a fixed index.\n  // `getMetaPage` locates it from React Query's page params and falls back to\n  // the last page when meta skipping is off.\n  const metaPage = getMetaPage(data);\n  const facets = capabilities.facets ? metaPage?.meta?.facets : undefined;\n  const chartData = capabilities.chart ? metaPage?.meta?.chartData : undefined;\n\n  const accessors = React.useMemo(\n    () => createRowAccessors<RemoteRow>(manifest),\n    [manifest],\n  );\n\n  const dynamicFilterFields = React.useMemo(\n    () => applyFacets(filterFields, facets),\n    [filterFields, facets],\n  );\n\n  const defaultColumnFilters = React.useMemo(() => {\n    const { sort: _sort, ...filters } = search;\n    return Object.entries(filters)\n      .map(([id, value]) => ({ id, value }))\n      .filter(({ value }) => {\n        if (value === null || value === undefined) return false;\n        if (Array.isArray(value) && value.length === 0) return false;\n        return true;\n      });\n  }, [search]);\n\n  const sort = (search.sort ?? manifest.defaults?.sort) as\n    | { id: string; desc: boolean }\n    | null\n    | undefined;\n\n  return (\n    <DataTableInfinite<RemoteRow>\n      columns={columns}\n      data={flatData}\n      filterFields={dynamicFilterFields}\n      defaultColumnFilters={defaultColumnFilters}\n      defaultColumnSorting={sort ? [sort] : undefined}\n      defaultColumnVisibility={defaultColumnVisibility}\n      // A count the server said it does not compute is left undefined rather\n      // than rendered as a confident zero.\n      totalRows={\n        capabilities.totalRowCount ? metaPage?.meta?.totalRowCount : undefined\n      }\n      filterRows={\n        capabilities.filterRowCount ? metaPage?.meta?.filterRowCount : undefined\n      }\n      totalRowsFetched={flatData.length}\n      isFetching={isFetching}\n      isLoading={isLoading}\n      hasNextPage={hasNextPage}\n      fetchNextPage={fetchNextPage}\n      // Backwards paging is what live mode is built on. Without it the button\n      // is not rendered at all, rather than rendered and inert.\n      {...(capabilities.backwardPagination ? { fetchPreviousPage } : {})}\n      refetch={refetch}\n      getRowId={accessors.getRowId}\n      {...(getRowClassName ? { getRowClassName } : {})}\n      // When the server cannot facet, these fall through to the table's own\n      // faceting over the rows already loaded.\n      {...(capabilities.facets\n        ? {\n            getFacetedUniqueValues: getFacetedUniqueValues(facets),\n            getFacetedMinMaxValues: getFacetedMinMaxValues(facets),\n          }\n        : {})}\n      {...(chartData && chartSlot\n        ? { chartSlot: chartSlot(chartData, manifest.chart) }\n        : {})}\n      {...(sheetSlot ? { sheetSlot: sheetSlot(sheetFields) } : {})}\n      tableId={tableId}\n      {...slots}\n    />\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:block"
}
