instaui
⚠️ Notice: This package is currently under development and is in beta. Features and functionality may change. Use it at your own risk, and feel free to contribute or report issues!
instaui is a zero-code CRUD UI generator for React, allowing you to create fully functional CRUD interfaces from any REST API with minimal setup. By simply passing in an API client and configuration object, instaui dynamically renders all necessary CRUD components and actions, saving you from writing repetitive code.
Features
- 🛠️ Instant CRUD UI: Generate CRUD interfaces by defining API endpoints and field configurations.
- 📄 Server-side Pagination: Efficiently handles large datasets using server-side pagination.
- 🔗 Relation Management: Supports related entities, seamlessly linking them within the UI.
- ✏️ Built-in Validation: Define custom validators for each field in the config.
- 🚀 Ant Design Integration: Uses Ant Design components for a polished UI out of the box.
- 🔄 Routing & Navigation: Integrated with React Router for seamless navigation.
- 🎨 Customizable UI: Customize headers, footers, and even replace entire components.
- 📱 Responsive Design: Works on both desktop and mobile devices.
Installation
Install instaui with npm or Yarn:
npm install instaui
yarn add instaui
Dependencies
instaui requires the following peer dependencies:
And uses the following libraries:
- antd 5.11.0+
- axios 1.6.0+
- react-router-dom 6.20.0+
- dayjs 1.11.10+
Basic Usage
Here's a basic example of how to use instaui:
import {ItemCrud} from 'instaui';
import axios from 'axios';
import {BrowserRouter, Routes, Route, Navigate} from 'react-router-dom';
const apiClient = axios.create({
baseURL: 'http://localhost:3000',
headers: {
Authorization: 'Bearer your-token',
'Content-Type': 'application/json',
},
});
const createApiClient = (axiosInstance) => {
return {
get: async (url, config) => {
const response = await axiosInstance.get(url, config);
return {status: 'success', data: response.data};
},
post: async (url, data, config) => {
const response = await axiosInstance.post(url, data, config);
return {status: 'success', data: response.data};
},
patch: async (url, data, config) => {
const response = await axiosInstance.patch(url, data, config);
return {status: 'success', data: response.data};
},
delete: async (url, config) => {
const response = await axiosInstance.delete(url, config);
return {status: 'success', data: response.data};
},
};
};
const endpoints = [
{
key: 'users',
label: 'Users',
url: '/users',
idField: 'id',
fields: [
{
key: 'id',
label: 'ID',
type: 'text',
required: true,
readOnly: true,
showInList: true,
},
{
key: 'name',
label: 'Name',
type: 'text',
required: true,
showInList: true,
patchable: true,
postable: true,
},
{
key: 'email',
label: 'Email',
type: 'email',
required: true,
showInList: true,
patchable: true,
postable: true,
},
],
validator: (values) => {
const errors = {};
if (!values.name) errors.name = 'Name is required';
if (!values.email) errors.email = 'Email is required';
return errors;
},
},
];
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to="/users" replace/>}/>
<Route
path="/:entity"
element={
<ItemCrud
apiClient={createApiClient(apiClient)}
config={{endpoints}}
useDrawer={false} // Use modal instead of drawer
/>
}
/>
<Route
path="/:entity/:operation/:id"
element={
<ItemCrud
apiClient={createApiClient(apiClient)}
config={{endpoints}}
useDrawer={false}
/>
}
/>
</Routes>
</BrowserRouter>
);
}
export default App;
Advanced Configuration
Endpoint Configuration
Each endpoint in the endpoints array can have the following properties:
interface EndpointConfig {
key: string;
label: string;
url: string;
idField?: string;
fields: FieldConfig[];
validator: (values: Record<string, unknown>) => Record<string, string>;
customComponent?: React.ComponentType;
header?: React.ReactNode;
footer?: React.ReactNode;
actionButtons?: ActionButtonConfig;
}
Field Configuration
Each field in the fields array can have the following properties:
interface FieldConfig {
key: string;
label: string;
type: 'text' | 'textarea' | 'number' | 'boolean' | 'date' | 'datetime' | 'time' | 'select' | 'relation' | 'url' | 'email';
required?: boolean;
readOnly?: boolean;
placeHolder?: string;
options?: { label: string; value: string }[];
relation?: RelationConfig;
showInList?: boolean;
sortable?: boolean;
filterable?: boolean;
filterType?: 'eq' | 'range' | 'boolean';
renderInList?: (value: any) => React.ReactNode;
renderInDetail?: (value: any) => React.ReactNode;
isFile?: boolean;
isImage?: boolean;
accept?: string | string[];
maxSize?: number;
postable?: boolean;
patchable?: boolean;
dateFormat?: string;
keepLocalTime?: boolean;
}
Relation Configuration
For fields of type relation, you can configure how the relation works:
interface RelationConfig {
entity: string;
idField: string;
keyColumns?: string[];
dropDownOptions?: (value: Record<string, unknown>) => { label: string; value: string };
}
Action Button Configuration
You can customize the action buttons for each endpoint:
interface ActionButtonConfig {
show?: boolean;
edit?: {
show?: boolean;
text?: string;
icon?: ReactNode;
};
delete?: {
show?: boolean;
text?: string;
icon?: ReactNode;
};
}
Custom Rendering
You can customize how fields are rendered in both list and detail views:
const formatCurrency = (value) => {
if (typeof value === 'number') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
}
return String(value);
};
{
key: 'price',
label
:
'Price',
type
:
'number',
required
:
true,
showInList
:
true,
patchable
:
true,
postable
:
true,
renderInList
:
formatCurrency,
renderInDetail
:
formatCurrency,
}
Custom Components
You can replace the entire CRUD UI for an endpoint with your own custom component:
const CustomComponent = () => {
return (
<div>
<h1>Custom Component</h1>
<p>This is a completely custom component for this endpoint.</p>
</div>
);
};
{
key: 'custom',
label
:
'Custom',
url
:
'/custom',
customComponent
:
CustomComponent,
fields
:
[],
}
Custom Headers and Footers
You can add custom headers and footers to any endpoint:
const CustomHeader = () => {
return (
<div style={{padding: '16px', background: '#f0f2f5', marginBottom: '24px'}}>
<h2>Custom Header</h2>
<p>This is a custom header for this endpoint.</p>
</div>
);
};
const CustomFooter = () => {
return (
<div style={{padding: '16px', background: '#f0f2f5', marginTop: '24px'}}>
<p>Custom Footer © {new Date().getFullYear()}</p>
</div>
);
};
{
key: 'users',
label
:
'Users',
url
:
'/users',
header
:
<CustomHeader/>,
footer
:
<CustomFooter/>,
}
Complete Example
For a complete example of how to use instaui, see the example below:
import {App as AntApp, Card, ConfigProvider, Typography} from 'antd';
import {BrowserRouter, Navigate, Route, Routes} from 'react-router-dom';
import {EndpointConfig, ItemCrud} from 'instaui';
import axios from 'axios';
import React from 'react';
import {createApiClient} from './utils/apiAdapter';
const formatCurrency = (value) => {
if (typeof value === 'number') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
}
return String(value);
};
const formatStatus = (value) => {
const statusMap = {
active: {color: 'green', text: 'Active'},
inactive: {color: 'red', text: 'Inactive'},
pending: {color: 'orange', text: 'Pending'},
completed: {color: 'blue', text: 'Completed'},
};
const statusValue = String(value).toLowerCase();
const status = statusMap[statusValue] || statusMap['inactive'];
return (
<span style={{color: status.color, fontWeight: 'bold'}}>
{status.text}
</span>
);
};
const CustomItemsComponent = () => {
const {Title, Paragraph, Text} = Typography;
return (
<div style={{padding: '20px'}}>
<div style={{marginBottom: '20px'}}>
<Title level={2}>Custom Items View</Title>
<Paragraph>
This is a custom component for the Items endpoint. It demonstrates how to use custom renders.
</Paragraph>
</div>
<div style={{display: 'flex', flexWrap: 'wrap', gap: '16px'}}>
<Card
title="Sample Item"
style={{width: 300}}
hoverable
>
<p><Text strong>Description:</Text> This is a sample item description.</p>
<p><Text strong>Price:</Text> {formatCurrency(99.99)}</p>
<p><Text strong>Status:</Text> {formatStatus('active')}</p>
</Card>
<Card
title="Another Item"
style={{width: 300}}
hoverable
>
<p><Text strong>Description:</Text> Another sample item description.</p>
<p><Text strong>Price:</Text> {formatCurrency(149.99)}</p>
<p><Text strong>Status:</Text> {formatStatus('pending')}</p>
</Card>
</div>
</div>
);
};
const CustomHeader = () => {
const {Title} = Typography;
return (
<div style={{
padding: '16px',
background: '#f0f2f5',
borderRadius: '4px',
marginBottom: '24px'
}}>
<Title level={2} style={{margin: 0}}>Custom Header</Title>
<p>This is a custom header that can be used with any endpoint.</p>
</div>
);
};
const CustomFooter = () => {
return (
<div style={{
padding: '16px',
background: '#f0f2f5',
borderRadius: '4px',
marginTop: '24px',
textAlign: 'center'
}}>
<p style={{margin: 0}}>instaui © {new Date().getFullYear()}</p>
</div>
);
};
const AppHeader = () => {
const {Title} = Typography;
return (
<div style={{
padding: '20px',
background: '#001529',
color: 'white',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}>
<Title level={2} style={{color: 'white', margin: 0}}>Item CRUD Application</Title>
<p style={{color: 'rgba(255, 255, 255, 0.65)', margin: '8px 0 0 0'}}>
A React-based CRUD application with dynamic form generation
</p>
</div>
);
};
const AppFooter = () => {
return (
<div style={{
padding: '16px',
background: '#001529',
color: 'white',
textAlign: 'center'
}}>
<p style={{margin: 0}}>
instaui © {new Date().getFullYear()} | All Rights Reserved
</p>
</div>
);
};
function App() {
const axiosInstance = axios.create({
baseURL: 'http://localhost:3000',
headers: {
Authorization: 'Bearer your-token',
'Content-Type': 'application/json',
},
});
const apiClient = createApiClient(axiosInstance);
const useDrawer = false;
const endpoints = [
{
key: 'users',
label: 'Users',
url: '/users',
idField: 'uid',
header: <div style={{padding: '16px', background: '#e6f7ff', borderRadius: '4px', marginBottom: '24px'}}>
<Typography.Title level={2} style={{margin: 0}}>Users Management</Typography.Title>
<Typography.Paragraph>
This endpoint uses the default CRUD component but with a custom header and footer.
</Typography.Paragraph>
</div>,
footer: <div style={{
padding: '16px',
background: '#e6f7ff',
borderRadius: '4px',
marginTop: '24px',
textAlign: 'center'
}}>
<Typography.Text>Users data is managed according to our privacy policy.</Typography.Text>
</div>,
actionButtons: {
edit: {
text: 'Modify User',
},
delete: {
text: 'Remove User',
},
},
fields: [
{
key: 'uid',
label: 'ID',
type: 'text',
required: true,
readOnly: true,
showInList: true,
filterable: true,
filterType: 'eq',
},
{
key: 'fname',
label: 'First Name',
type: 'text',
required: true,
showInList: true,
patchable: true,
postable: true,
filterable: true,
sortable: true,
filterType: 'eq',
},
{
key: 'lname',
label: 'Last Name',
type: 'text',
required: true,
showInList: true,
patchable: true,
postable: true,
filterable: true,
filterType: 'eq',
},
{
key: 'date',
label: 'Date',
type: 'date',
required: true,
showInList: true,
patchable: true,
postable: true,
filterable: true,
filterType: 'range',
},
{
key: 'date2',
label: 'Date2',
type: 'datetime',
required: true,
showInList: true,
patchable: true,
postable: true,
filterable: true,
filterType: 'range',
keepLocalTime: false,
},
{
key: 'photo',
label: 'Photo',
type: 'url',
required: false,
showInList: true,
patchable: true,
postable: true,
isImage: true,
},
{
key: 'status',
label: 'Status',
type: 'boolean',
required: true,
showInList: true,
patchable: true,
postable: true,
filterable: true,
filterType: 'boolean',
},
],
validator: (values) => {
const errors = {};
if (!values.fname) errors.fname = 'First name is required';
if (!values.lname) errors.lname = 'Last name is required';
if (!values.email) errors.email = 'Email is required';
return errors;
},
},
{
key: 'items',
label: 'Items',
url: '/items',
idField: 'uid',
header: <CustomHeader/>,
footer: <CustomFooter/>,
actionButtons: {
delete: {
show: false
}
},
fields: [
{
key: 'uid',
label: 'ID',
type: 'text',
required: true,
readOnly: true,
showInList: true,
filterable: true,
filterType: 'eq',
},
{
key: 'name',
label: 'Name',
type: 'text',
required: true,
showInList: true,
patchable: true,
postable: true,
filterable: true,
filterType: 'eq',
},
{
key: 'price',
label: 'Price',
type: 'number',
required: true,
showInList: true,
patchable: true,
postable: true,
renderInList: formatCurrency,
renderInDetail: formatCurrency,
filterable: true,
filterType: 'range',
},
{
key: 'status',
label: 'Status',
type: 'select',
options: [
{label: 'Active', value: 'active'},
{label: 'Inactive', value: 'inactive'},
],
required: true,
showInList: true,
patchable: true,
postable: true,
filterable: true,
filterType: 'eq',
renderInList: formatStatus,
renderInDetail: formatStatus,
},
],
validator: (values) => {
const errors = {};
if (!values.name) errors.name = 'Name is required';
if (!values.price) errors.price = 'Price is required';
if (!values.status) errors.status = 'Status is required';
return errors;
},
},
{
key: 'cards',
label: 'Cards',
url: '/cards',
idField: 'uid',
customComponent: CustomItemsComponent,
fields: [],
},
];
return (
<ConfigProvider
theme={{
token: {
colorPrimary: '#1890ff',
},
}}>
<AntApp notification={{placement: 'bottomRight', duration: 5}}>
<BrowserRouter>
<div
style={{
width: '100vw',
height: '100vh',
overflow: 'scroll',
display: 'flex',
flexDirection: 'column',
}}>
{/* Application Header */}
<AppHeader/>
{/* Main Content */}
<div>
<Routes>
<Route path='/' element={<Navigate to='/users' replace/>}/>
<Route
path='/:entity'
element={
<ItemCrud
apiClient={apiClient}
config={{endpoints}}
useDrawer={useDrawer}
/>
}
/>
<Route
path='/:entity/:operation/:id'
element={
<ItemCrud
apiClient={apiClient}
config={{endpoints}}
useDrawer={useDrawer}
/>
}
/>
</Routes>
</div>
{/* Application Footer */}
<AppFooter/>
</div>
</BrowserRouter>
</AntApp>
</ConfigProvider>
);
}
export default App;
Documentation
For more detailed documentation, please see the docs folder:
Contributing
We welcome contributions to instaui. Please feel free to submit issues and pull requests on our GitHub repository.
License
This project is licensed under the MIT License.