@openenvx/admin
v0.1.0
Published
Runtime-generated admin panel for Drizzle ORM using Refine and shadcn/ui
Downloads
16
Readme
@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
# 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.jsonQuick Start
1. Create Data Provider (Server)
// 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)
// 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
// 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
// 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
// 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.
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.
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 layoutCreateView/CreateViewHeader- Create page layoutEditView/EditViewHeader- Edit page layoutShowView/ShowViewHeader- Show page layout
Data Table
DataTable- Advanced table with sorting, filtering, paginationDataTableSorter- Column sort buttonsDataTableFilterDropdownText- Text filter dropdownDataTableColumnHeader- Column header with sort/filter
Buttons
CreateButton- Navigate to create pageEditButton- Navigate to edit pageDeleteButton- Delete with confirmationShowButton- Navigate to show pageListButton- Navigate to list pageSaveButton- Save formRefreshButton- Refresh data
Fields
TextField- Display textDateField- Display datesBooleanField- Display booleansNumberField- Display numbersEmailField- Display emailsUrlField- Display URLsTagField- Display tagsMarkdownField- Display markdown
Layout
Layout- Complete app layout with sidebar, header, theme supportThemedLayout- Layout with theme providerBreadcrumb- Navigation breadcrumb
Forms
Form- Form wrapper with validationAutoSaveIndicator- Shows auto-save status
Auth
SignInForm- Login formSignUpForm- Registration formForgotPasswordForm- Password reset
Utilities
ErrorComponent- Error boundaryNotificationProvider- Toast notificationsCanAccess- Access control wrapperAuthenticated- Auth guard wrapper
Resources
License
MIT
