Skip to main content

Built-in Components

DEVELOPER Beginner

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 app
  • extraPublicRoutes (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 to hasPermission from usePermission)
  • permissions (string[], optional): Multiple codes; combined with checkAll
  • checkAll (boolean, optional, default false): If true, require all codes in permissions; if false, require any
  • fallback (ReactNode, optional, default null): Rendered when the user lacks access
  • children (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 to useAI().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} />;

Next Steps​


Need help? Ask in GitHub Discussions.