Built-in Components
Use pre-built React components from EZ-Console framework.
Overviewβ
EZ-Console provides a set of built-in React components that simplify common development tasks. These components include the main application shell, permission guards, layouts, and more. This guide covers how to use these components in your applications.
The authoritative list of every symbol exported from 'ez-console' matches web/src/index.ts β see Package exports (index.ts).
Built-in Componentsβ
EZAppβ
The main application component that provides the entire app structure, including layout, navigation, authentication flow, and route protection.
import { EZApp } from 'ez-console';
export default function App() {
return <EZApp basePath='/' />;
}
Propsβ
basePath(string): Base path for the application (e.g.,'/'or'/app')extraPrivateRoutes(Route[]): Additional private routes to add to the appextraPublicRoutes(Route[]): Additional public routes (e.g., login, register)
Example: Adding Custom Routesβ
import { EZApp, withSuspense } from 'ez-console';
import { lazy } from 'react';
const ProductPage = lazy(() => import('@/pages/ProductPage'));
const OrderPage = lazy(() => import('@/pages/OrderPage'));
export default function App() {
return (
<EZApp
basePath='/'
extraPrivateRoutes={[
{
path: '/products',
element: withSuspense(ProductPage),
name: 'products',
},
{
path: '/orders',
element: withSuspense(OrderPage),
name: 'orders',
},
]}
/>
);
}
Built-in Featuresβ
The EZApp component provides:
- β Layout (header, sidebar, content area)
- β Navigation menu with automatic route detection
- β Authentication flow and route protection
- β User profile dropdown menu
- β Language switcher
- β Global state providers (Auth, Theme, etc.)
PermissionGuardβ
Conditionally renders content based on user permissions. This component integrates with EZ-Console's RBAC system.
import { PermissionGuard } from 'ez-console';
export const ProductActions: React.FC = () => {
return (
<Space>
<PermissionGuard permission="product:create">
<Button type="primary">Create Product</Button>
</PermissionGuard>
<PermissionGuard permission="product:update">
<Button>Edit Product</Button>
</PermissionGuard>
<PermissionGuard permission="product:delete">
<Button danger>Delete Product</Button>
</PermissionGuard>
</Space>
);
};
Propsβ
permission(string, optional): A single permission code (same codes you pass tohasPermissionfromusePermission)permissions(string[], optional): Multiple codes; combined withcheckAllcheckAll(boolean, optional, defaultfalse): Iftrue, require all codes inpermissions; iffalse, require anyfallback(ReactNode, optional, defaultnull): Rendered when the user lacks accesschildren(ReactNode): Content to render when access is granted
For imperative checks (buttons, hooks), use usePermission. For admin-only UI, use AdminGuard.
AppLayoutβ
The admin shell (header, collapsible sidebar, breadcrumbs, org switcher, AI entry points) is implemented as AppLayout, not Ant Designβs Layout primitive. EZApp wires it internally; advanced integrations can reuse it or replace rendering via EZAppβs renderLayout prop (see Application shell).
import type { AppLayoutProps } from 'ez-console';
// Typical usage is indirect through EZApp; props include `routes`, `menuStyle`, `aiChatProps`, β¦
PrivateRouteβ
Wraps an element with authentication and optional RBAC. Exported as PrivateRouteProps: element, optional requiredPermission, optional requiredPermissions (all must pass when multiple are set in source β verify your version).
import { PrivateRoute } from 'ez-console';
import { Route } from 'react-router-dom';
<Route
path="/reports"
element={
<PrivateRoute
element={<ReportsPage />}
requiredPermission="report:view"
/>
}
/>;
// Multiple codes: user must satisfy **all** (uses hasAllPermissions)
<PrivateRoute
element={<AdminReports />}
requiredPermissions={['report:view', 'report:export']}
/>;
Table (data table)β
Framework Table extends Ant Designβs table with a required request callback that receives pagination params and returns API.PaginationResponse<T>, keeping paging in sync with the server.
import { Table, apiGet } from 'ez-console';
<Table<API.Product>
rowKey="id"
columns={columns}
request={(params) =>
apiGet<API.PaginationResponse<API.Product>>('/products', {
params: { current: params.current, page_size: params.page_size },
})
}
/>;
Use ref with TableRef / TableActionRefProps to call reload() after mutations.
Actions (toolbar buttons)β
Renders a horizontal set of actions with optional permission wrapping, confirm dialogs, and overflow Dropdown when maxVisibleItems is exceeded. Each action is an ActionProps object (must include a stable key).
import { Actions } from 'ez-console';
<Actions
maxVisibleItems={3}
actions={[
{ key: 'add', label: 'Add', type: 'primary', permission: 'product:create', onClick: handleAdd },
{ key: 'export', label: 'Export', onClick: handleExport },
]}
/>
AI shell helpersβ
Besides embedding AIChat, the package exports layout glue from the same module as the shell:
AIChatModalβ full-width modal host tied touseAI().visible.AIChatSiderβ resizable sidebar host.AIChatButtonβ floating action button to open the assistant.
Pass AIChatProps through EZApp aiChatProps or to these hosts when extending behavior.
MarkdownViewer and MarkdownCodeβ
MarkdownViewer renders Markdown content (props: MarkdownViewerProps). For fenced code blocks, the package re-exports the internal code renderer as MarkdownCode (exported name in index.ts: Code as MarkdownCode).
JsonSchemaConfigFormβ
Use JsonSchemaConfigForm (and JsonSchemaConfigFormItem for Ant Design Form.Item layouts) to edit JSON-shaped configuration using a JSON Schema document and RJSF widgets β suitable for dynamic admin settings.
Forbidden and NotFoundβ
Ready-made page components for 403 and 404 routes; the built-in router tree registers /403 and /404 paths. You can reuse them in extraPublicRoutes.
Loadingβ
Simple loading spinner component for indicating loading states.
import { Loading } from 'ez-console';
export const MyComponent: React.FC = () => {
const { loading } = useRequest(() => apiGet('/data'));
if (loading) {
return <Loading />;
}
return <div>Content</div>;
};
Creating Reusable Componentsβ
While EZ-Console provides core components, you'll often want to create your own reusable components. Here are some common patterns:
DataTable Componentβ
A reusable table component with action buttons:
// src/components/DataTable.tsx
import { Table, Button, Space } from 'antd';
import { TableProps } from 'antd/es/table';
import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
interface DataTableProps<T> extends TableProps<T> {
onEdit?: (record: T) => void;
onDelete?: (record: T) => void;
canEdit?: boolean;
canDelete?: boolean;
}
export function DataTable<T extends { id: string }>({
onEdit,
onDelete,
canEdit = false,
canDelete = false,
columns,
...props
}: DataTableProps<T>) {
const actionColumn = {
title: 'Actions',
key: 'actions',
fixed: 'right' as const,
width: 150,
render: (_: any, record: T) => (
<Space>
{canEdit && onEdit && (
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => onEdit(record)}
>
Edit
</Button>
)}
{canDelete && onDelete && (
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => onDelete(record)}
>
Delete
</Button>
)}
</Space>
),
};
const finalColumns = [...(columns || []), actionColumn];
return <Table columns={finalColumns} {...props} />;
}
Usageβ
import { DataTable } from '@/components/DataTable';
export const ProductList: React.FC = () => {
const { data, loading } = useRequest(() => apiGet('/products'));
const handleEdit = (product: Product) => {
// Navigate to edit page
};
const handleDelete = (product: Product) => {
// Show delete confirmation
};
return (
<DataTable
dataSource={data}
loading={loading}
columns={[
{ title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Price', dataIndex: 'price', key: 'price' },
]}
onEdit={handleEdit}
onDelete={handleDelete}
canEdit={true}
canDelete={true}
/>
);
};
FormModal Componentβ
A reusable modal component with form handling:
// src/components/FormModal.tsx
import { Modal, Form, FormInstance } from 'antd';
import { ReactNode } from 'react';
interface FormModalProps {
title: string;
open: boolean;
loading?: boolean;
form: FormInstance;
onOk: () => void;
onCancel: () => void;
children: ReactNode;
width?: number;
}
export const FormModal: React.FC<FormModalProps> = ({
title,
open,
loading,
form,
onOk,
onCancel,
children,
width = 520,
}) => {
return (
<Modal
title={title}
open={open}
onOk={onOk}
onCancel={onCancel}
confirmLoading={loading}
width={width}
destroyOnClose
>
<Form form={form} layout="vertical">
{children}
</Form>
</Modal>
);
};
Usageβ
import { FormModal } from '@/components/FormModal';
export const ProductForm: React.FC = () => {
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const { run: createProduct, loading } = useRequest(
(values) => apiPost('/products', values),
{ manual: true, onSuccess: () => setOpen(false) }
);
const handleSubmit = () => {
form.validateFields().then((values) => {
createProduct(values);
});
};
return (
<FormModal
title="Create Product"
open={open}
loading={loading}
form={form}
onOk={handleSubmit}
onCancel={() => setOpen(false)}
>
<Form.Item name="name" label="Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
</FormModal>
);
};
SearchForm Componentβ
A reusable search form component:
// src/components/SearchForm.tsx
import { Form, Input, Button, Row, Col, Space } from 'antd';
import { FormInstance } from 'antd/es/form';
import { useTranslation } from 'ez-console';
interface SearchFormProps {
form: FormInstance;
onSearch: (values: any) => void;
onReset: () => void;
children?: React.ReactNode;
}
export const SearchForm: React.FC<SearchFormProps> = ({
form,
onSearch,
onReset,
children,
}) => {
const { t } = useTranslation();
return (
<Form form={form} layout="vertical" onFinish={onSearch}>
<Row gutter={16}>
{children}
</Row>
<Row style={{ marginTop: 16 }}>
<Col>
<Space>
<Button type="primary" htmlType="submit">
{t('common.search')}
</Button>
<Button onClick={onReset}>
{t('common.reset')}
</Button>
</Space>
</Col>
</Row>
</Form>
);
};
Usageβ
import { SearchForm } from '@/components/SearchForm';
export const ProductSearch: React.FC = () => {
const [form] = Form.useForm();
const handleSearch = (values: any) => {
// Perform search
};
const handleReset = () => {
form.resetFields();
handleSearch({});
};
return (
<SearchForm form={form} onSearch={handleSearch} onReset={handleReset}>
<Col span={8}>
<Form.Item name="name" label="Name">
<Input placeholder="Search by name" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="category" label="Category">
<Select placeholder="Select category">
<Option value="electronics">Electronics</Option>
<Option value="clothing">Clothing</Option>
</Select>
</Form.Item>
</Col>
</SearchForm>
);
};
ConfirmDelete Hookβ
A custom hook for delete confirmation dialogs:
// src/components/ConfirmDelete.tsx
import { Modal } from 'antd';
import { useTranslation } from 'ez-console';
import { ExclamationCircleOutlined } from '@ant-design/icons';
export const useConfirmDelete = () => {
const { t } = useTranslation();
return (onConfirm: () => void, itemName?: string) => {
Modal.confirm({
title: t('common.confirmDelete'),
icon: <ExclamationCircleOutlined />,
content: itemName
? t('common.confirmDeleteItem', { item: itemName })
: t('common.confirmDeleteMessage'),
okText: t('common.confirm'),
okType: 'danger',
cancelText: t('common.cancel'),
onOk: onConfirm,
});
};
};
Usageβ
import { useConfirmDelete } from '@/components/ConfirmDelete';
export const ProductList: React.FC = () => {
const confirmDelete = useConfirmDelete();
const { run: deleteProduct } = useRequest(
(id: string) => apiDelete(`/products/${id}`),
{ manual: true }
);
const handleDelete = (product: Product) => {
confirmDelete(() => {
deleteProduct(product.id);
}, product.name);
};
return (
<Button danger onClick={() => handleDelete(product)}>
Delete
</Button>
);
};
Component Patternsβ
List-Detail Patternβ
A common pattern where clicking an item shows its details:
// src/pages/products/index.tsx
import { useState } from 'react';
import ProductList from './ProductList';
import ProductDetail from './ProductDetail';
export const ProductsPage: React.FC = () => {
const [selectedId, setSelectedId] = useState<string | null>(null);
if (selectedId) {
return (
<ProductDetail
id={selectedId}
onBack={() => setSelectedId(null)}
/>
);
}
return (
<ProductList
onSelect={(id) => setSelectedId(id)}
/>
);
};
Master-Detail Patternβ
Using tabs to show different aspects of a single entity:
// src/pages/orders/OrderDetail.tsx
import { Tabs } from 'antd';
import OrderInfo from './OrderInfo';
import OrderItems from './OrderItems';
import OrderHistory from './OrderHistory';
export const OrderDetail: React.FC<{ id: string }> = ({ id }) => {
return (
<Tabs
items={[
{
key: 'info',
label: 'Order Information',
children: <OrderInfo id={id} />,
},
{
key: 'items',
label: 'Items',
children: <OrderItems id={id} />,
},
{
key: 'history',
label: 'History',
children: <OrderHistory id={id} />,
},
]}
/>
);
};
Form with Stepsβ
Multi-step forms for complex data entry:
// src/pages/products/ProductWizard.tsx
import { Steps, Button, Card } from 'antd';
import { useState } from 'react';
export const ProductWizard: React.FC = () => {
const [current, setCurrent] = useState(0);
const [formData, setFormData] = useState({});
const steps = [
{
title: 'Basic Info',
content: <BasicInfoForm data={formData} onChange={setFormData} />,
},
{
title: 'Pricing',
content: <PricingForm data={formData} onChange={setFormData} />,
},
{
title: 'Images',
content: <ImagesForm data={formData} onChange={setFormData} />,
},
];
return (
<Card>
<Steps current={current} items={steps} style={{ marginBottom: 24 }} />
<div>{steps[current].content}</div>
<div style={{ marginTop: 24, textAlign: 'right' }}>
{current > 0 && (
<Button onClick={() => setCurrent(current - 1)} style={{ marginRight: 8 }}>
Previous
</Button>
)}
{current < steps.length - 1 && (
<Button type="primary" onClick={() => setCurrent(current + 1)}>
Next
</Button>
)}
{current === steps.length - 1 && (
<Button type="primary" onClick={handleSubmit}>
Submit
</Button>
)}
</div>
</Card>
);
};
Styling Componentsβ
Using Ant Design Themeβ
Global theme customization through ConfigProvider:
// src/App.tsx
import { ConfigProvider } from 'antd';
import { EZApp } from 'ez-console';
export default function App() {
return (
<ConfigProvider
theme={{
token: {
colorPrimary: '#1890ff',
borderRadius: 6,
fontSize: 14,
},
components: {
Button: {
colorPrimary: '#00b96b',
},
Table: {
headerBg: '#fafafa',
},
},
}}
>
<EZApp basePath='/' />
</ConfigProvider>
);
}
Inline Stylesβ
For component-specific styling:
<div style={{
padding: 24,
background: '#fff',
minHeight: 360,
borderRadius: 8,
}}>
Content
</div>
CSS Modulesβ
For more complex styling needs:
// styles.module.css
.container {
padding: 24px;
background: #fff;
border-radius: 8px;
}
.title {
font-size: 20px;
font-weight: 600;
margin-bottom: 16px;
}
// Component.tsx
import styles from './styles.module.css';
<div className={styles.container}>
<h2 className={styles.title}>Title</h2>
Content
</div>
Best Practicesβ
1. Component Compositionβ
Break down complex components into smaller, reusable pieces:
// β
Good: Composed components
<ProductCard>
<ProductImage />
<ProductInfo />
<ProductActions />
</ProductCard>
// β Bad: Monolithic component
<ProductCardWithEverything />
2. TypeScript Typingβ
Always define TypeScript interfaces for component props:
// β
Good
interface ProductCardProps {
product: Product;
onEdit?: (product: Product) => void;
onDelete?: (id: string) => void;
}
// β Bad
const ProductCard = (props: any) => { ... }
3. Default Propsβ
Provide sensible defaults:
// β
Good
interface ButtonProps {
size?: 'small' | 'medium' | 'large';
variant?: 'primary' | 'secondary';
}
const Button = ({ size = 'medium', variant = 'primary', ...props }: ButtonProps) => {
// ...
};
4. Error Boundariesβ
Wrap components in error boundaries for better error handling:
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary fallback={<ErrorFallback />}>
<ProductList />
</ErrorBoundary>
5. Loading Statesβ
Always show loading indicators during async operations:
const { data, loading } = useRequest(() => apiGet('/products'));
if (loading) {
return <Loading />;
}
return <ProductList data={data} />;
6. Empty Statesβ
Handle empty data gracefully:
const { data } = useRequest(() => apiGet('/products'));
if (!data || data.length === 0) {
return <Empty description="No products found" />;
}
return <ProductList data={data} />;
Related Topicsβ
- Application Shell - Learn about the EZApp component
- Routing & Navigation - Add routes to your application
- Forms & Validation - Create forms with validation
- Internationalization - Add multi-language support
- State Management - Manage application state
Next Stepsβ
- Learn about Internationalization
- Explore API Integration
- Review Performance Optimization
Need help? Ask in GitHub Discussions.