npm package: @openenvx/admin
Description: Runtime-generated admin panel for Drizzle ORM using Refine and shadcn/ui
Author: OpenEnvX
Latest version: 0.1.0

# @openenvx/admin

Zero-config admin panel for Drizzle ORM powered by Refine and shadcn/ui.

## Features

- **Zero Configuration**: Works out of the box with your Drizzle schema
- **Runtime Generated**: Admin UI updates automatically when schema changes
- **Full CRUD**: List, create, edit, and delete operations for all tables
- **Type Safe**: Full TypeScript support with Drizzle type inference
- **Customizable**: Override any part of the UI or behavior
- **Modern Stack**: Built on Refine, shadcn/ui, and Tailwind CSS

## Installation

```bash
# Install @openenvx/admin and required peer dependencies
npm install @openenvx/admin @refinedev/core @refinedev/react-table @tanstack/react-table drizzle-orm

# Install Refine's shadcn/ui components
npx shadcn@latest add https://ui.refine.dev/r/views.json
npx shadcn@latest add https://ui.refine.dev/r/data-table.json
npx shadcn@latest add https://ui.refine.dev/r/layout/layout-01.json
npx shadcn@latest add https://ui.refine.dev/r/buttons.json
```

## Quick Start

### 1. Create Data Provider (Server)

```typescript
// app/api/admin/[...resource]/route.ts
import { createDrizzleDataProvider } from '@openenvx/admin/server';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from '@/db/schema';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool, { schema });

// Create data provider with resource mapping
const dataProvider = createDrizzleDataProvider({
  db,
  resources: {
    users: 'users',
    posts: 'posts',
    // Map resource names to table names
  }
});

// Export route handlers
export const { GET, POST, PUT, DELETE } = dataProvider;
```

### 2. Set up Refine with Admin Resources (Client)

```typescript
// app/admin/layout.tsx
'use client';

import { Refine } from '@refinedev/core';
import routerProvider from '@refinedev/nextjs-router';
import dataProvider from '@refinedev/simple-rest';
import { Layout } from '@/components/refine-ui/layout/layout-01';
import { createAdminResources } from '@openenvx/admin';
import * as schema from '@/db/schema';

const resources = createAdminResources({
  schema,
  exclude: ['_drizzle_migrations'],
  resources: {
    users: {
      label: 'Team Members',
      meta: { icon: 'Users' }
    },
    posts: {
      label: 'Blog Posts',
      meta: { icon: 'FileText' }
    }
  }
});

export default function AdminLayout({ children }: { children: React.ReactNode }) {
  return (
    <Refine
      routerProvider={routerProvider}
      dataProvider={dataProvider('/api/admin')}
      resources={resources}
      options={{
        syncWithLocation: true,
        warnWhenUnsavedChanges: true,
        useNewQueryKeys: true,
        projectId: 'my-admin',
      }}
    >
      <Layout>{children}</Layout>
    </Refine>
  );
}
```

### 3. Create List Page using Refine's shadcn/ui Components

```typescript
// app/admin/[resource]/page.tsx
'use client';

import { useMemo } from 'react';
import { useTable } from '@refinedev/react-table';
import type { ColumnDef } from '@tanstack/react-table';
import { useResourceParams } from '@refinedev/core';
import { ListView, ListViewHeader } from '@/components/refine-ui/views/list-view';
import { DataTable } from '@/components/refine-ui/data-table/data-table';
import { DataTableSorter } from '@/components/refine-ui/data-table/data-table-sorter';
import { DataTableFilterDropdownText } from '@/components/refine-ui/data-table/data-table-filter';
import { CreateButton } from '@/components/refine-ui/buttons/create-button';

interface Post {
  id: number;
  title: string;
  status: string;
  createdAt: string;
}

export default function PostListPage() {
  const { resource } = useResourceParams();

  const columns = useMemo<ColumnDef<Post>[]>(
    () => [
      {
        id: 'id',
        accessorKey: 'id',
        header: ({ column }) => (
          <div className="flex items-center gap-1">
            <span>ID</span>
            <DataTableSorter column={column} />
          </div>
        ),
      },
      {
        id: 'title',
        accessorKey: 'title',
        header: ({ column, table }) => (
          <div className="flex items-center gap-1">
            <span>Title</span>
            <DataTableFilterDropdownText
              defaultOperator="contains"
              column={column}
              table={table}
              placeholder="Filter by title"
            />
          </div>
        ),
      },
      {
        id: 'status',
        accessorKey: 'status',
        header: 'Status',
      },
      {
        id: 'createdAt',
        accessorKey: 'createdAt',
        header: ({ column }) => (
          <div className="flex items-center gap-1">
            <span>Created</span>
            <DataTableSorter column={column} />
          </div>
        ),
        cell: ({ row }) => new Date(row.original.createdAt).toLocaleDateString(),
      },
    ],
    []
  );

  const table = useTable<Post>({
    columns,
    refineCoreProps: {
      resource: resource?.name || 'posts',
    },
  });

  return (
    <ListView>
      <ListViewHeader
        title={resource?.meta?.label || resource?.name || 'Posts'}
        headerButtons={<CreateButton resource={resource?.name} />}
      />
      <DataTable table={table} />
    </ListView>
  );
}
```

### 4. Create Edit Page

```typescript
// app/admin/[resource]/edit/[id]/page.tsx
'use client';

import { useForm } from '@refinedev/react-hook-form';
import { useResourceParams } from '@refinedev/core';
import { EditView, EditViewHeader } from '@/components/refine-ui/views/edit-view';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';

export default function PostEditPage() {
  const { resource } = useResourceParams();
  const {
    refineCore: { onFinish, formLoading },
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    refineCoreProps: {
      resource: resource?.name,
    },
  });

  return (
    <EditView>
      <EditViewHeader title={`Edit ${resource?.meta?.label || resource?.name}`} />
      <form onSubmit={handleSubmit(onFinish)}>
        <div className="space-y-4">
          <div>
            <Label htmlFor="title">Title</Label>
            <Input
              id="title"
              {...register('title', { required: 'Title is required' })}
            />
            {errors.title && (
              <span className="text-red-500 text-sm">{errors.title.message}</span>
            )}
          </div>

          <div>
            <Label htmlFor="status">Status</Label>
            <Select {...register('status')}>
              <SelectTrigger>
                <SelectValue placeholder="Select status" />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="draft">Draft</SelectItem>
                <SelectItem value="published">Published</SelectItem>
              </SelectContent>
            </Select>
          </div>

          <div>
            <Label htmlFor="content">Content</Label>
            <textarea
              id="content"
              {...register('content')}
              className="w-full min-h-[200px] p-2 border rounded"
            />
          </div>
        </div>
      </form>
    </EditView>
  );
}
```

### 5. Create Show Page

```typescript
// app/admin/[resource]/show/[id]/page.tsx
'use client';

import { useShow } from '@refinedev/core';
import { useResourceParams } from '@refinedev/core';
import { ShowView, ShowViewHeader } from '@/components/refine-ui/views/show-view';
import { TextField } from '@/components/refine-ui/fields/text-field';
import { DateField } from '@/components/refine-ui/fields/date-field';

interface Post {
  id: number;
  title: string;
  content: string;
  status: string;
  createdAt: string;
  updatedAt: string;
}

export default function PostShowPage() {
  const { resource } = useResourceParams();
  const { queryResult } = useShow<Post>({
    resource: resource?.name,
  });

  const { data, isLoading } = queryResult;
  const record = data?.data;

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return (
    <ShowView>
      <ShowViewHeader title={record?.title} />
      <div className="space-y-4">
        <div>
          <label className="font-medium">Title</label>
          <TextField value={record?.title} />
        </div>
        <div>
          <label className="font-medium">Status</label>
          <div className="capitalize">{record?.status}</div>
        </div>
        <div>
          <label className="font-medium">Content</label>
          <div className="whitespace-pre-wrap">{record?.content}</div>
        </div>
        <div className="grid grid-cols-2 gap-4">
          <div>
            <label className="font-medium">Created</label>
            <DateField value={record?.createdAt} />
          </div>
          <div>
            <label className="font-medium">Updated</label>
            <DateField value={record?.updatedAt} />
          </div>
        </div>
      </div>
    </ShowView>
  );
}
```

## Configuration

### `createAdminResources(options)`

Generate Refine resources from your Drizzle schema.

```typescript
const resources = createAdminResources({
  // Your Drizzle schema exports
  schema,
  
  // Tables to exclude
  exclude: ['migrations', 'sessions'],
  
  // Custom configurations per resource
  resources: {
    users: {
      label: 'Team Members',      // Display name
      icon: 'Users',              // Lucide icon name
      hidden: false,              // Hide from navigation
      meta: {                     // Custom metadata
        icon: 'Users',
        label: 'Team Members',
        canDelete: false,         // Disable delete button
      }
    }
  },
  
  // Default icon for resources
  defaultIcon: 'FileText'
});
```

### `createDrizzleDataProvider(options)`

Create a Refine data provider for Drizzle ORM.

```typescript
const dataProvider = createDrizzleDataProvider({
  // Drizzle database instance with query support
  db,
  
  // Resource name to table name mapping
  resources: {
    users: 'users',
    posts: 'posts',
    // 'resource-name': 'table_name'
  }
});
```

## Available Refine shadcn/ui Components

### Views
- `ListView` / `ListViewHeader` - List page layout
- `CreateView` / `CreateViewHeader` - Create page layout
- `EditView` / `EditViewHeader` - Edit page layout
- `ShowView` / `ShowViewHeader` - Show page layout

### Data Table
- `DataTable` - Advanced table with sorting, filtering, pagination
- `DataTableSorter` - Column sort buttons
- `DataTableFilterDropdownText` - Text filter dropdown
- `DataTableColumnHeader` - Column header with sort/filter

### Buttons
- `CreateButton` - Navigate to create page
- `EditButton` - Navigate to edit page
- `DeleteButton` - Delete with confirmation
- `ShowButton` - Navigate to show page
- `ListButton` - Navigate to list page
- `SaveButton` - Save form
- `RefreshButton` - Refresh data

### Fields
- `TextField` - Display text
- `DateField` - Display dates
- `BooleanField` - Display booleans
- `NumberField` - Display numbers
- `EmailField` - Display emails
- `UrlField` - Display URLs
- `TagField` - Display tags
- `MarkdownField` - Display markdown

### Layout
- `Layout` - Complete app layout with sidebar, header, theme support
- `ThemedLayout` - Layout with theme provider
- `Breadcrumb` - Navigation breadcrumb

### Forms
- `Form` - Form wrapper with validation
- `AutoSaveIndicator` - Shows auto-save status

### Auth
- `SignInForm` - Login form
- `SignUpForm` - Registration form
- `ForgotPasswordForm` - Password reset

### Utilities
- `ErrorComponent` - Error boundary
- `NotificationProvider` - Toast notifications
- `CanAccess` - Access control wrapper
- `Authenticated` - Auth guard wrapper

## Resources

- [Refine Documentation](https://refine.dev/)
- [Refine shadcn/ui Integration](https://refine.dev/core/docs/ui-integrations/shadcn/introduction/)
- [shadcn/ui Documentation](https://ui.shadcn.com/)
- [Drizzle ORM Documentation](https://orm.drizzle.team/)

## License

MIT