decent
This commit is contained in:
21785
package-lock.json
generated
Normal file
21785
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "skybridge",
|
||||
"version": "1.0.0",
|
||||
"description": "Skybridge monorepo - All-in-one startup platform",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"web-components",
|
||||
"web",
|
||||
"kms/web",
|
||||
"faas/web",
|
||||
"user/web",
|
||||
"demo"
|
||||
],
|
||||
"scripts": {
|
||||
"build:components": "npm run build --workspace=web-components",
|
||||
"build:all": "npm run build --workspaces --if-present",
|
||||
"dev:shell": "npm run dev --workspace=web",
|
||||
"dev:kms": "npm run dev --workspace=kms/web",
|
||||
"dev:faas": "npm run dev --workspace=faas/web",
|
||||
"dev:user": "npm run dev --workspace=user/web",
|
||||
"dev:demo": "npm run dev --workspace=demo",
|
||||
"install:all": "npm install --workspaces",
|
||||
"clean": "npm run clean --workspaces --if-present"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=9.0.0"
|
||||
}
|
||||
}
|
||||
282
web-components/INTEGRATION_EXAMPLE.md
Normal file
282
web-components/INTEGRATION_EXAMPLE.md
Normal file
@ -0,0 +1,282 @@
|
||||
# Integration Example
|
||||
|
||||
This document shows how to integrate the `@skybridge/web-components` library into existing microfrontends.
|
||||
|
||||
## 1. Update Package.json
|
||||
|
||||
Add the component library as a dependency:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@skybridge/web-components": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Example: Refactoring User Management
|
||||
|
||||
Here's how to refactor the existing `UserSidebar.tsx` to use the shared components:
|
||||
|
||||
### Before (existing code):
|
||||
```tsx
|
||||
// user/web/src/components/UserSidebar.tsx
|
||||
import { Paper, TextInput, Select, Button /* ... */ } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
// ... lots of boilerplate form logic
|
||||
```
|
||||
|
||||
### After (using shared components):
|
||||
```tsx
|
||||
// user/web/src/components/UserSidebar.tsx
|
||||
import React from 'react';
|
||||
import {
|
||||
FormSidebar,
|
||||
FormField,
|
||||
validateRequired,
|
||||
validateEmail,
|
||||
combineValidators
|
||||
} from '@skybridge/web-components';
|
||||
import { userService } from '../services/userService';
|
||||
|
||||
const UserSidebar: React.FC<UserSidebarProps> = ({
|
||||
opened,
|
||||
onClose,
|
||||
onSuccess,
|
||||
editUser,
|
||||
}) => {
|
||||
const fields: FormField[] = [
|
||||
{
|
||||
name: 'first_name',
|
||||
label: 'First Name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'Enter first name',
|
||||
},
|
||||
{
|
||||
name: 'last_name',
|
||||
label: 'Last Name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'Enter last name',
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
type: 'email',
|
||||
required: true,
|
||||
placeholder: 'Enter email address',
|
||||
validation: { email: true },
|
||||
},
|
||||
{
|
||||
name: 'role',
|
||||
label: 'Role',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'admin', label: 'Admin' },
|
||||
{ value: 'moderator', label: 'Moderator' },
|
||||
{ value: 'user', label: 'User' },
|
||||
{ value: 'viewer', label: 'Viewer' },
|
||||
],
|
||||
defaultValue: 'user',
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'inactive', label: 'Inactive' },
|
||||
{ value: 'suspended', label: 'Suspended' },
|
||||
{ value: 'pending', label: 'Pending' },
|
||||
],
|
||||
defaultValue: 'pending',
|
||||
},
|
||||
];
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
if (editUser) {
|
||||
await userService.updateUser(editUser.id, values);
|
||||
} else {
|
||||
await userService.createUser(values);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormSidebar
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
title="User"
|
||||
editMode={!!editUser}
|
||||
editItem={editUser}
|
||||
fields={fields}
|
||||
onSubmit={handleSubmit}
|
||||
width={400}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 3. Example: User Management Table
|
||||
|
||||
Replace the existing table with the shared DataTable:
|
||||
|
||||
```tsx
|
||||
// user/web/src/components/UserManagement.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
DataTable,
|
||||
TableColumn,
|
||||
useApiService,
|
||||
useDataFilter,
|
||||
Badge
|
||||
} from '@skybridge/web-components';
|
||||
|
||||
const UserManagement: React.FC = () => {
|
||||
const [sidebarOpened, setSidebarOpened] = useState(false);
|
||||
const [editUser, setEditUser] = useState(null);
|
||||
|
||||
const {
|
||||
data: users,
|
||||
loading,
|
||||
error,
|
||||
getAll,
|
||||
delete: deleteUser,
|
||||
refresh,
|
||||
} = useApiService({
|
||||
baseURL: 'http://localhost:8090/api',
|
||||
defaultHeaders: { 'X-User-Email': 'admin@example.com' },
|
||||
}, 'users');
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{
|
||||
key: 'first_name',
|
||||
label: 'First Name',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: 'last_name',
|
||||
label: 'Last Name',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: 'Email',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: 'role',
|
||||
label: 'Role',
|
||||
render: (value) => (
|
||||
<Badge color="blue" size="sm">{value}</Badge>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status'
|
||||
// Uses default status rendering from DataTable
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
getAll();
|
||||
}, [getAll]);
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditUser(null);
|
||||
setSidebarOpened(true);
|
||||
};
|
||||
|
||||
const handleEdit = (user) => {
|
||||
setEditUser(user);
|
||||
setSidebarOpened(true);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
setSidebarOpened(false);
|
||||
setEditUser(null);
|
||||
refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
data={users}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
error={error}
|
||||
title="User Management"
|
||||
searchable
|
||||
onAdd={handleAdd}
|
||||
onEdit={handleEdit}
|
||||
onDelete={deleteUser}
|
||||
onRefresh={refresh}
|
||||
emptyMessage="No users found"
|
||||
/>
|
||||
|
||||
<UserSidebar
|
||||
opened={sidebarOpened}
|
||||
onClose={() => setSidebarOpened(false)}
|
||||
onSuccess={handleSuccess}
|
||||
editUser={editUser}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 4. Benefits of Integration
|
||||
|
||||
### Code Reduction
|
||||
- **UserSidebar.tsx**: Reduced from ~250 lines to ~80 lines
|
||||
- **UserManagement.tsx**: Cleaner, more focused on business logic
|
||||
- **Removed Duplication**: No more repeated form validation, notification logic
|
||||
|
||||
### Consistency
|
||||
- All forms look and behave the same across microfrontends
|
||||
- Standardized validation messages and error handling
|
||||
- Consistent table layouts and interactions
|
||||
|
||||
### Maintainability
|
||||
- Bug fixes in shared components benefit all microfrontends
|
||||
- New features added once, available everywhere
|
||||
- Easier to update UI themes and styling
|
||||
|
||||
## 5. Installation Steps
|
||||
|
||||
1. **Install the component library**:
|
||||
```bash
|
||||
npm install @skybridge/web-components --workspace=user/web
|
||||
```
|
||||
|
||||
2. **Update imports in existing components**:
|
||||
```tsx
|
||||
// Replace individual Mantine imports
|
||||
import { FormSidebar, DataTable, useApiService } from '@skybridge/web-components';
|
||||
```
|
||||
|
||||
3. **Refactor components gradually**:
|
||||
- Start with new components
|
||||
- Refactor existing components one at a time
|
||||
- Test thoroughly in each microfrontend
|
||||
|
||||
4. **Update build configuration** (if needed):
|
||||
- Ensure the component library is built before microfrontends
|
||||
- Update webpack externals if necessary
|
||||
|
||||
## 6. Migration Checklist
|
||||
|
||||
- [ ] Add `@skybridge/web-components` to package.json
|
||||
- [ ] Refactor sidebar forms to use `FormSidebar`
|
||||
- [ ] Replace tables with `DataTable` component
|
||||
- [ ] Use shared validation utilities
|
||||
- [ ] Standardize notification handling
|
||||
- [ ] Update API service patterns to use `useApiService`
|
||||
- [ ] Test all CRUD operations
|
||||
- [ ] Verify styling consistency
|
||||
- [ ] Update tests if necessary
|
||||
|
||||
This integration will significantly reduce code duplication while improving consistency and maintainability across all Skybridge microfrontends.
|
||||
315
web-components/README.md
Normal file
315
web-components/README.md
Normal file
@ -0,0 +1,315 @@
|
||||
# Skybridge Web Components
|
||||
|
||||
A shared component library for Skybridge microfrontends, providing consistent UI components, hooks, and utilities across all applications.
|
||||
|
||||
## Installation
|
||||
|
||||
Since this is a monorepo package, install it as a workspace dependency:
|
||||
|
||||
```bash
|
||||
npm install @skybridge/web-components
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### FormSidebar
|
||||
|
||||
A reusable sidebar form component that handles create/edit operations with validation.
|
||||
|
||||
```tsx
|
||||
import { FormSidebar, FormField } from '@skybridge/web-components';
|
||||
|
||||
const fields: FormField[] = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'Enter name',
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
type: 'email',
|
||||
required: true,
|
||||
validation: { email: true },
|
||||
},
|
||||
{
|
||||
name: 'role',
|
||||
label: 'Role',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: 'admin', label: 'Admin' },
|
||||
{ value: 'user', label: 'User' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const MyComponent = () => {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [editItem, setEditItem] = useState(null);
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
// Handle create/update logic
|
||||
await apiService.create(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormSidebar
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
onSuccess={() => {
|
||||
setOpened(false);
|
||||
// Refresh data
|
||||
}}
|
||||
title="User"
|
||||
editMode={!!editItem}
|
||||
editItem={editItem}
|
||||
fields={fields}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### DataTable
|
||||
|
||||
A feature-rich data table component with filtering, pagination, and actions.
|
||||
|
||||
```tsx
|
||||
import { DataTable, TableColumn } from '@skybridge/web-components';
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{ key: 'name', label: 'Name', sortable: true },
|
||||
{ key: 'email', label: 'Email', sortable: true },
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
render: (value) => (
|
||||
<Badge color={value === 'active' ? 'green' : 'gray'}>
|
||||
{value}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const MyTable = () => {
|
||||
const [data, setData] = useState([]);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
title="Users"
|
||||
searchable
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
onAdd={() => {/* Handle add */}}
|
||||
onEdit={(item) => {/* Handle edit */}}
|
||||
onDelete={async (item) => {/* Handle delete */}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Hooks
|
||||
|
||||
### useApiService
|
||||
|
||||
A comprehensive API service hook with CRUD operations and state management.
|
||||
|
||||
```tsx
|
||||
import { useApiService } from '@skybridge/web-components';
|
||||
|
||||
const MyComponent = () => {
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
getAll,
|
||||
create,
|
||||
update,
|
||||
delete: deleteItem,
|
||||
refresh,
|
||||
} = useApiService({
|
||||
baseURL: 'http://localhost:8080/api',
|
||||
defaultHeaders: { 'X-User-Email': 'admin@example.com' },
|
||||
}, 'users');
|
||||
|
||||
useEffect(() => {
|
||||
getAll();
|
||||
}, [getAll]);
|
||||
|
||||
const handleCreate = async (userData) => {
|
||||
await create(userData);
|
||||
};
|
||||
|
||||
return (
|
||||
// Your component JSX
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### useDataFilter
|
||||
|
||||
Client-side filtering and search functionality.
|
||||
|
||||
```tsx
|
||||
import { useDataFilter } from '@skybridge/web-components';
|
||||
|
||||
const MyComponent = () => {
|
||||
const [rawData, setRawData] = useState([]);
|
||||
|
||||
const {
|
||||
filteredData,
|
||||
filters,
|
||||
setFilter,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
clearFilters,
|
||||
} = useDataFilter(rawData, {
|
||||
searchFields: ['name', 'email', 'description'],
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search..."
|
||||
/>
|
||||
{/* Render filteredData */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Utilities
|
||||
|
||||
### Notifications
|
||||
|
||||
Standardized notification helpers.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
showSuccessNotification,
|
||||
showErrorNotification,
|
||||
showCrudNotification,
|
||||
NotificationMessages,
|
||||
} from '@skybridge/web-components';
|
||||
|
||||
// Simple notifications
|
||||
showSuccessNotification('Operation completed!');
|
||||
showErrorNotification('Something went wrong');
|
||||
|
||||
// CRUD operation notifications
|
||||
showCrudNotification.success('create', 'User');
|
||||
showCrudNotification.error('update', 'User', 'Custom error message');
|
||||
|
||||
// Pre-defined messages
|
||||
showSuccessNotification(NotificationMessages.userCreated);
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
Common validation functions and patterns.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
validateRequired,
|
||||
validateEmail,
|
||||
validateDuration,
|
||||
combineValidators,
|
||||
ValidationPatterns,
|
||||
parseDuration,
|
||||
} from '@skybridge/web-components';
|
||||
|
||||
// Single validators
|
||||
const emailError = validateEmail('invalid-email'); // Returns error message or null
|
||||
|
||||
// Combined validators
|
||||
const validateName = combineValidators(
|
||||
validateRequired,
|
||||
(value) => validateMinLength(value, 2, 'Name')
|
||||
);
|
||||
|
||||
// Duration parsing (common in KMS/FaaS)
|
||||
const seconds = parseDuration('24h'); // Returns 86400
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build the library
|
||||
npm run build
|
||||
|
||||
# Watch mode for development
|
||||
npm run dev
|
||||
|
||||
# Type checking
|
||||
npm run typecheck
|
||||
|
||||
# Linting
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Usage in Microfrontends
|
||||
|
||||
1. Add as a dependency in your microfrontend's `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@skybridge/web-components": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Import and use components:
|
||||
|
||||
```tsx
|
||||
import { FormSidebar, DataTable, useApiService } from '@skybridge/web-components';
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The component library is designed to:
|
||||
|
||||
- **Standardize UI**: Consistent components across all microfrontends
|
||||
- **Reduce Duplication**: Shared business logic and utilities
|
||||
- **Improve Maintainability**: Single source of truth for common patterns
|
||||
- **Ensure Consistency**: Unified validation, notifications, and API handling
|
||||
|
||||
## Common Patterns Extracted
|
||||
|
||||
Based on analysis of the existing microfrontends, this library extracts these common patterns:
|
||||
|
||||
1. **Sidebar Forms**: All microfrontends use similar slide-out forms
|
||||
2. **Data Tables**: Consistent table layouts with actions and filtering
|
||||
3. **API Integration**: Standard CRUD operations with error handling
|
||||
4. **Validation**: Common validation rules and patterns
|
||||
5. **Notifications**: Standardized success/error messaging
|
||||
6. **Filtering**: Client-side search and filter functionality
|
||||
|
||||
## Compatibility
|
||||
|
||||
- React 18+
|
||||
- TypeScript 5+
|
||||
- Mantine 7.0+
|
||||
- Works with all existing Skybridge microfrontends (web, kms, user, faas)
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new components or utilities:
|
||||
|
||||
1. Follow existing patterns and naming conventions
|
||||
2. Add TypeScript types for all props and return values
|
||||
3. Include documentation and usage examples
|
||||
4. Test with all microfrontends before committing
|
||||
5. Update this README with new features
|
||||
45
web-components/dist/components/DataTable/DataTable.d.ts
vendored
Normal file
45
web-components/dist/components/DataTable/DataTable.d.ts
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { ListItem, FilterOptions } from '../../types';
|
||||
export interface TableColumn {
|
||||
key: string;
|
||||
label: string;
|
||||
sortable?: boolean;
|
||||
filterable?: boolean;
|
||||
width?: string | number;
|
||||
render?: (value: any, item: ListItem) => React.ReactNode;
|
||||
}
|
||||
export interface TableAction {
|
||||
key: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
color?: string;
|
||||
onClick: (item: ListItem) => void;
|
||||
show?: (item: ListItem) => boolean;
|
||||
}
|
||||
export interface DataTableProps {
|
||||
data: ListItem[];
|
||||
columns: TableColumn[];
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
title?: string;
|
||||
total?: number;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
onPageChange?: (page: number) => void;
|
||||
onAdd?: () => void;
|
||||
onEdit?: (item: ListItem) => void;
|
||||
onDelete?: (item: ListItem) => Promise<void>;
|
||||
onRefresh?: () => void;
|
||||
customActions?: TableAction[];
|
||||
searchable?: boolean;
|
||||
filterable?: boolean;
|
||||
filters?: FilterOptions;
|
||||
onFiltersChange?: (filters: FilterOptions) => void;
|
||||
withBorder?: boolean;
|
||||
withColumnBorders?: boolean;
|
||||
striped?: boolean;
|
||||
highlightOnHover?: boolean;
|
||||
emptyMessage?: string;
|
||||
}
|
||||
declare const DataTable: React.FC<DataTableProps>;
|
||||
export default DataTable;
|
||||
17
web-components/dist/components/FormSidebar/FormSidebar.d.ts
vendored
Normal file
17
web-components/dist/components/FormSidebar/FormSidebar.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { FormField } from '../../types';
|
||||
export interface FormSidebarProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
title: string;
|
||||
editMode?: boolean;
|
||||
editItem?: any;
|
||||
fields: FormField[];
|
||||
onSubmit: (values: any) => Promise<void>;
|
||||
width?: number;
|
||||
initialValues?: Record<string, any>;
|
||||
validateOnSubmit?: boolean;
|
||||
}
|
||||
declare const FormSidebar: React.FC<FormSidebarProps>;
|
||||
export default FormSidebar;
|
||||
25
web-components/dist/hooks/useApiService.d.ts
vendored
Normal file
25
web-components/dist/hooks/useApiService.d.ts
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { FilterOptions } from '../types';
|
||||
export interface ApiServiceConfig {
|
||||
baseURL: string;
|
||||
defaultHeaders?: Record<string, string>;
|
||||
timeout?: number;
|
||||
}
|
||||
export interface UseApiServiceReturn<T> {
|
||||
data: T[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
client: AxiosInstance;
|
||||
getAll: (filters?: FilterOptions) => Promise<T[]>;
|
||||
getById: (id: string) => Promise<T>;
|
||||
create: (data: Partial<T>) => Promise<T>;
|
||||
update: (id: string, data: Partial<T>) => Promise<T>;
|
||||
delete: (id: string) => Promise<void>;
|
||||
clearError: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
export declare const useApiService: <T extends {
|
||||
id: string;
|
||||
}>(config: ApiServiceConfig, endpoint: string) => UseApiServiceReturn<T>;
|
||||
16
web-components/dist/hooks/useDataFilter.d.ts
vendored
Normal file
16
web-components/dist/hooks/useDataFilter.d.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
import { FilterOptions, ListItem } from '../types';
|
||||
export interface UseDataFilterOptions {
|
||||
searchFields?: string[];
|
||||
defaultFilters?: FilterOptions;
|
||||
debounceMs?: number;
|
||||
}
|
||||
export interface UseDataFilterReturn {
|
||||
filteredData: ListItem[];
|
||||
filters: FilterOptions;
|
||||
setFilter: (key: string, value: any) => void;
|
||||
clearFilters: () => void;
|
||||
resetFilters: () => void;
|
||||
searchTerm: string;
|
||||
setSearchTerm: (term: string) => void;
|
||||
}
|
||||
export declare const useDataFilter: (data: ListItem[], options?: UseDataFilterOptions) => UseDataFilterReturn;
|
||||
13
web-components/dist/index.d.ts
vendored
Normal file
13
web-components/dist/index.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
export { default as FormSidebar } from './components/FormSidebar/FormSidebar';
|
||||
export { default as DataTable } from './components/DataTable/DataTable';
|
||||
export * from './types';
|
||||
export { useApiService } from './hooks/useApiService';
|
||||
export { useDataFilter } from './hooks/useDataFilter';
|
||||
export * from './utils/notifications';
|
||||
export * from './utils/validation';
|
||||
export { Paper, Stack, Group, Button, TextInput, Select, MultiSelect, NumberInput, Textarea, JsonInput, ActionIcon, Menu, Text, Title, Badge, Table, Pagination, LoadingOverlay, Center, Box, ScrollArea, Divider, } from '@mantine/core';
|
||||
export { useDisclosure, useToggle, useLocalStorage, } from '@mantine/hooks';
|
||||
export { useForm } from '@mantine/form';
|
||||
export { notifications } from '@mantine/notifications';
|
||||
export { modals } from '@mantine/modals';
|
||||
export { IconPlus, IconEdit, IconTrash, IconSearch, IconFilter, IconRefresh, IconX, IconDots, IconChevronDown, IconChevronRight, IconUser, IconUsers, IconKey, IconSettings, IconEye, IconEyeOff, IconCopy, IconCheck, IconAlertCircle, IconInfoCircle, } from '@tabler/icons-react';
|
||||
2
web-components/dist/index.esm.js
vendored
Normal file
2
web-components/dist/index.esm.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web-components/dist/index.esm.js.map
vendored
Normal file
1
web-components/dist/index.esm.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
web-components/dist/index.js
vendored
Normal file
2
web-components/dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web-components/dist/index.js.map
vendored
Normal file
1
web-components/dist/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
79
web-components/dist/types/index.d.ts
vendored
Normal file
79
web-components/dist/types/index.d.ts
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
export interface BaseEntity {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
export interface Owner {
|
||||
type: 'individual' | 'team';
|
||||
name: string;
|
||||
owner: string;
|
||||
}
|
||||
export interface FormSidebarProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
editItem?: any;
|
||||
}
|
||||
export interface ListItem {
|
||||
id: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
email?: string;
|
||||
status?: string;
|
||||
role?: string;
|
||||
type?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
has_more: boolean;
|
||||
}
|
||||
export interface FilterOptions {
|
||||
search?: string;
|
||||
status?: string;
|
||||
type?: string;
|
||||
role?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
export interface NotificationConfig {
|
||||
title: string;
|
||||
message: string;
|
||||
color: 'red' | 'green' | 'blue' | 'yellow' | 'gray';
|
||||
[key: string]: any;
|
||||
}
|
||||
export interface ValidationRule {
|
||||
required?: boolean;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
pattern?: RegExp;
|
||||
email?: boolean;
|
||||
url?: boolean;
|
||||
custom?: (value: any) => string | null;
|
||||
}
|
||||
export interface FormField {
|
||||
name: string;
|
||||
label: string;
|
||||
type: 'text' | 'email' | 'number' | 'select' | 'multiselect' | 'textarea' | 'date' | 'json';
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
options?: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
}>;
|
||||
validation?: ValidationRule;
|
||||
defaultValue?: any;
|
||||
}
|
||||
37
web-components/dist/utils/notifications.d.ts
vendored
Normal file
37
web-components/dist/utils/notifications.d.ts
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
export declare const showSuccessNotification: (message: string, title?: string) => void;
|
||||
export declare const showErrorNotification: (message: string, title?: string) => void;
|
||||
export declare const showWarningNotification: (message: string, title?: string) => void;
|
||||
export declare const showInfoNotification: (message: string, title?: string) => void;
|
||||
export declare const NotificationMessages: {
|
||||
createSuccess: (entityName: string) => string;
|
||||
updateSuccess: (entityName: string) => string;
|
||||
deleteSuccess: (entityName: string) => string;
|
||||
createError: (entityName: string) => string;
|
||||
updateError: (entityName: string) => string;
|
||||
deleteError: (entityName: string) => string;
|
||||
loadError: (entityName: string) => string;
|
||||
networkError: string;
|
||||
validationError: string;
|
||||
requiredFieldError: (fieldName: string) => string;
|
||||
authRequired: string;
|
||||
permissionDenied: string;
|
||||
sessionExpired: string;
|
||||
applicationCreated: string;
|
||||
applicationUpdated: string;
|
||||
applicationDeleted: string;
|
||||
tokenCreated: string;
|
||||
tokenRevoked: string;
|
||||
userCreated: string;
|
||||
userUpdated: string;
|
||||
userDeleted: string;
|
||||
functionCreated: string;
|
||||
functionUpdated: string;
|
||||
functionDeleted: string;
|
||||
executionStarted: string;
|
||||
executionCompleted: string;
|
||||
executionFailed: string;
|
||||
};
|
||||
export declare const showCrudNotification: {
|
||||
success: (operation: "create" | "update" | "delete", entityName: string) => void;
|
||||
error: (operation: "create" | "update" | "delete" | "load", entityName: string, customMessage?: string) => void;
|
||||
};
|
||||
37
web-components/dist/utils/validation.d.ts
vendored
Normal file
37
web-components/dist/utils/validation.d.ts
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
export declare const ValidationPatterns: {
|
||||
email: RegExp;
|
||||
url: RegExp;
|
||||
duration: RegExp;
|
||||
token: RegExp;
|
||||
appId: RegExp;
|
||||
uuid: RegExp;
|
||||
};
|
||||
export declare const ValidationMessages: {
|
||||
required: (fieldName: string) => string;
|
||||
email: string;
|
||||
url: string;
|
||||
duration: string;
|
||||
minLength: (fieldName: string, minLength: number) => string;
|
||||
maxLength: (fieldName: string, maxLength: number) => string;
|
||||
pattern: (fieldName: string) => string;
|
||||
token: string;
|
||||
appId: string;
|
||||
uuid: string;
|
||||
positiveNumber: string;
|
||||
range: (fieldName: string, min: number, max: number) => string;
|
||||
};
|
||||
export declare const validateRequired: (value: any) => string | null;
|
||||
export declare const validateEmail: (value: string) => string | null;
|
||||
export declare const validateUrl: (value: string) => string | null;
|
||||
export declare const validateDuration: (value: string) => string | null;
|
||||
export declare const validateMinLength: (value: string, minLength: number, fieldName?: string) => string | null;
|
||||
export declare const validateMaxLength: (value: string, maxLength: number, fieldName?: string) => string | null;
|
||||
export declare const validatePattern: (value: string, pattern: RegExp, fieldName?: string) => string | null;
|
||||
export declare const validateRange: (value: number, min: number, max: number, fieldName?: string) => string | null;
|
||||
export declare const validateAppId: (value: string) => string | null;
|
||||
export declare const validateToken: (value: string) => string | null;
|
||||
export declare const validateUuid: (value: string) => string | null;
|
||||
export declare const validateJsonString: (value: string) => string | null;
|
||||
export declare const parseDuration: (duration: string) => number;
|
||||
export declare const formatDuration: (seconds: number) => string;
|
||||
export declare const combineValidators: (...validators: Array<(value: any) => string | null>) => (value: any) => string | null;
|
||||
75
web-components/package.json
Normal file
75
web-components/package.json
Normal file
@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "@skybridge/web-components",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Shared component library for Skybridge microfrontends",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"build:watch": "rollup -c -w",
|
||||
"dev": "rollup -c -w",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/core": "^7.0.0",
|
||||
"@mantine/hooks": "^7.0.0",
|
||||
"@mantine/notifications": "^7.0.0",
|
||||
"@mantine/form": "^7.0.0",
|
||||
"@mantine/dates": "^7.0.0",
|
||||
"@mantine/modals": "^7.0.0",
|
||||
"@mantine/code-highlight": "^7.0.0",
|
||||
"@tabler/icons-react": "^2.40.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"axios": "^1.11.0",
|
||||
"dayjs": "^1.11.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.0",
|
||||
"@babel/preset-react": "^7.22.0",
|
||||
"@babel/preset-typescript": "^7.22.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@rollup/plugin-babel": "^6.0.3",
|
||||
"@rollup/plugin-commonjs": "^25.0.3",
|
||||
"@rollup/plugin-node-resolve": "^15.1.0",
|
||||
"@rollup/plugin-typescript": "^11.1.2",
|
||||
"rollup": "^3.26.3",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"@rollup/plugin-terser": "^0.4.3",
|
||||
"typescript": "^5.1.0",
|
||||
"rimraf": "^5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-plugin-react": "^7.32.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yourusername/skybridge.git",
|
||||
"directory": "web-components"
|
||||
},
|
||||
"keywords": [
|
||||
"react",
|
||||
"components",
|
||||
"mantine",
|
||||
"typescript",
|
||||
"skybridge",
|
||||
"microfrontend"
|
||||
],
|
||||
"author": "Skybridge Team",
|
||||
"license": "MIT"
|
||||
}
|
||||
65
web-components/rollup.config.js
Normal file
65
web-components/rollup.config.js
Normal file
@ -0,0 +1,65 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import babel from '@rollup/plugin-babel';
|
||||
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import postcss from 'rollup-plugin-postcss';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
const packageJson = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
|
||||
export default {
|
||||
input: 'src/index.ts',
|
||||
output: [
|
||||
{
|
||||
file: packageJson.main,
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
},
|
||||
{
|
||||
file: packageJson.module,
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
peerDepsExternal(),
|
||||
resolve({
|
||||
browser: true,
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
}),
|
||||
babel({
|
||||
babelHelpers: 'bundled',
|
||||
exclude: 'node_modules/**',
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
presets: [
|
||||
['@babel/preset-react', { runtime: 'automatic' }],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
}),
|
||||
postcss({
|
||||
extract: false,
|
||||
modules: false,
|
||||
use: ['sass'],
|
||||
}),
|
||||
terser(),
|
||||
],
|
||||
external: [
|
||||
'react',
|
||||
'react-dom',
|
||||
'@mantine/core',
|
||||
'@mantine/hooks',
|
||||
'@mantine/notifications',
|
||||
'@mantine/form',
|
||||
'@mantine/dates',
|
||||
'@mantine/modals',
|
||||
'@mantine/code-highlight',
|
||||
'@tabler/icons-react',
|
||||
'axios',
|
||||
'dayjs',
|
||||
],
|
||||
};
|
||||
382
web-components/src/components/DataTable/DataTable.tsx
Normal file
382
web-components/src/components/DataTable/DataTable.tsx
Normal file
@ -0,0 +1,382 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Paper,
|
||||
Group,
|
||||
Button,
|
||||
TextInput,
|
||||
Select,
|
||||
ActionIcon,
|
||||
Menu,
|
||||
Text,
|
||||
Badge,
|
||||
Box,
|
||||
Stack,
|
||||
Pagination,
|
||||
LoadingOverlay,
|
||||
Center,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconSearch,
|
||||
IconPlus,
|
||||
IconDots,
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
IconRefresh,
|
||||
IconFilter,
|
||||
} from '@tabler/icons-react';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { ListItem, FilterOptions } from '../../types';
|
||||
|
||||
export interface TableColumn {
|
||||
key: string;
|
||||
label: string;
|
||||
sortable?: boolean;
|
||||
filterable?: boolean;
|
||||
width?: string | number;
|
||||
render?: (value: any, item: ListItem) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface TableAction {
|
||||
key: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
color?: string;
|
||||
onClick: (item: ListItem) => void;
|
||||
show?: (item: ListItem) => boolean;
|
||||
}
|
||||
|
||||
export interface DataTableProps {
|
||||
data: ListItem[];
|
||||
columns: TableColumn[];
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
title?: string;
|
||||
|
||||
// Pagination
|
||||
total?: number;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
onPageChange?: (page: number) => void;
|
||||
|
||||
// Actions
|
||||
onAdd?: () => void;
|
||||
onEdit?: (item: ListItem) => void;
|
||||
onDelete?: (item: ListItem) => Promise<void>;
|
||||
onRefresh?: () => void;
|
||||
customActions?: TableAction[];
|
||||
|
||||
// Filtering & Search
|
||||
searchable?: boolean;
|
||||
filterable?: boolean;
|
||||
filters?: FilterOptions;
|
||||
onFiltersChange?: (filters: FilterOptions) => void;
|
||||
|
||||
// Styling
|
||||
withBorder?: boolean;
|
||||
withColumnBorders?: boolean;
|
||||
striped?: boolean;
|
||||
highlightOnHover?: boolean;
|
||||
|
||||
// Empty state
|
||||
emptyMessage?: string;
|
||||
}
|
||||
|
||||
const DataTable: React.FC<DataTableProps> = ({
|
||||
data,
|
||||
columns,
|
||||
loading = false,
|
||||
error = null,
|
||||
title,
|
||||
total = 0,
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
onPageChange,
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onRefresh,
|
||||
customActions = [],
|
||||
searchable = true,
|
||||
filterable = false,
|
||||
filters = {},
|
||||
onFiltersChange,
|
||||
withBorder = true,
|
||||
withColumnBorders = false,
|
||||
striped = true,
|
||||
highlightOnHover = true,
|
||||
emptyMessage = 'No data available',
|
||||
}) => {
|
||||
const [localFilters, setLocalFilters] = useState<FilterOptions>(filters);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalFilters(filters);
|
||||
}, [filters]);
|
||||
|
||||
const handleFilterChange = (key: string, value: string) => {
|
||||
const newFilters = { ...localFilters, [key]: value };
|
||||
setLocalFilters(newFilters);
|
||||
onFiltersChange?.(newFilters);
|
||||
};
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
handleFilterChange('search', value);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = (item: ListItem) => {
|
||||
modals.openConfirmModal({
|
||||
title: 'Confirm Delete',
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Are you sure you want to delete this item? This action cannot be undone.
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: 'Delete', cancel: 'Cancel' },
|
||||
confirmProps: { color: 'red' },
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
if (onDelete) {
|
||||
await onDelete(item);
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Item deleted successfully',
|
||||
color: 'green',
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: error.message || 'Failed to delete item',
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderCellValue = (column: TableColumn, item: ListItem) => {
|
||||
const value = item[column.key];
|
||||
|
||||
if (column.render) {
|
||||
return column.render(value, item);
|
||||
}
|
||||
|
||||
// Default rendering for common data types
|
||||
if (value === null || value === undefined) {
|
||||
return <Text c="dimmed">-</Text>;
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
return (
|
||||
<Badge color={value ? 'green' : 'gray'} size="sm">
|
||||
{value ? 'Yes' : 'No'}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.key === 'status') {
|
||||
const statusColors: Record<string, string> = {
|
||||
active: 'green',
|
||||
inactive: 'gray',
|
||||
pending: 'yellow',
|
||||
suspended: 'red',
|
||||
success: 'green',
|
||||
error: 'red',
|
||||
warning: 'yellow',
|
||||
};
|
||||
return (
|
||||
<Badge color={statusColors[value] || 'blue'} size="sm">
|
||||
{value}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return <Text>{value.toString()}</Text>;
|
||||
};
|
||||
|
||||
const renderActionMenu = (item: ListItem) => {
|
||||
const actions: TableAction[] = [];
|
||||
|
||||
if (onEdit) {
|
||||
actions.push({
|
||||
key: 'edit',
|
||||
label: 'Edit',
|
||||
icon: <IconEdit size={14} />,
|
||||
onClick: onEdit,
|
||||
});
|
||||
}
|
||||
|
||||
if (onDelete) {
|
||||
actions.push({
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
icon: <IconTrash size={14} />,
|
||||
color: 'red',
|
||||
onClick: () => handleDeleteConfirm(item),
|
||||
});
|
||||
}
|
||||
|
||||
actions.push(...customActions);
|
||||
|
||||
const visibleActions = actions.filter(action =>
|
||||
!action.show || action.show(item)
|
||||
);
|
||||
|
||||
if (visibleActions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu position="bottom-end">
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="gray">
|
||||
<IconDots size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{visibleActions.map((action) => (
|
||||
<Menu.Item
|
||||
key={action.key}
|
||||
leftSection={action.icon}
|
||||
color={action.color}
|
||||
onClick={() => action.onClick(item)}
|
||||
>
|
||||
{action.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Header with title and actions */}
|
||||
<Group justify="space-between">
|
||||
<Group>
|
||||
{title && <Text size="xl" fw={600}>{title}</Text>}
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
{onRefresh && (
|
||||
<ActionIcon variant="light" onClick={onRefresh}>
|
||||
<IconRefresh size={16} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
|
||||
{onAdd && (
|
||||
<Button leftSection={<IconPlus size={16} />} onClick={onAdd}>
|
||||
Add New
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* Filters and Search */}
|
||||
{(searchable || filterable) && (
|
||||
<Group>
|
||||
{searchable && (
|
||||
<TextInput
|
||||
placeholder="Search..."
|
||||
leftSection={<IconSearch size={16} />}
|
||||
value={localFilters.search || ''}
|
||||
onChange={(e) => handleSearchChange(e.currentTarget.value)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{filterable && (
|
||||
<Group>
|
||||
<ActionIcon variant="light">
|
||||
<IconFilter size={16} />
|
||||
</ActionIcon>
|
||||
{/* Add specific filter components as needed */}
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{/* Table */}
|
||||
<Paper withBorder={withBorder} pos="relative">
|
||||
<LoadingOverlay visible={loading} />
|
||||
|
||||
{error ? (
|
||||
<Center p="xl">
|
||||
<Stack align="center" gap="xs">
|
||||
<Text c="red" fw={500}>Error loading data</Text>
|
||||
<Text c="dimmed" size="sm">{error}</Text>
|
||||
{onRefresh && (
|
||||
<Button variant="light" size="sm" onClick={onRefresh}>
|
||||
Try Again
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Center>
|
||||
) : data.length === 0 ? (
|
||||
<Center p="xl">
|
||||
<Stack align="center" gap="xs">
|
||||
<Text c="dimmed">{emptyMessage}</Text>
|
||||
{onAdd && (
|
||||
<Button variant="light" size="sm" onClick={onAdd}>
|
||||
Add First Item
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
<Table
|
||||
striped={striped}
|
||||
highlightOnHover={highlightOnHover}
|
||||
withColumnBorders={withColumnBorders}
|
||||
>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{columns.map((column) => (
|
||||
<Table.Th key={column.key} style={{ width: column.width }}>
|
||||
{column.label}
|
||||
</Table.Th>
|
||||
))}
|
||||
{(onEdit || onDelete || customActions.length > 0) && (
|
||||
<Table.Th style={{ width: 50 }}>Actions</Table.Th>
|
||||
)}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data.map((item) => (
|
||||
<Table.Tr key={item.id}>
|
||||
{columns.map((column) => (
|
||||
<Table.Td key={`${item.id}-${column.key}`}>
|
||||
{renderCellValue(column, item)}
|
||||
</Table.Td>
|
||||
))}
|
||||
{(onEdit || onDelete || customActions.length > 0) && (
|
||||
<Table.Td>
|
||||
{renderActionMenu(item)}
|
||||
</Table.Td>
|
||||
)}
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<Group justify="center">
|
||||
<Pagination
|
||||
total={totalPages}
|
||||
value={page}
|
||||
onChange={onPageChange}
|
||||
size="sm"
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataTable;
|
||||
247
web-components/src/components/FormSidebar/FormSidebar.tsx
Normal file
247
web-components/src/components/FormSidebar/FormSidebar.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Paper,
|
||||
TextInput,
|
||||
Select,
|
||||
MultiSelect,
|
||||
NumberInput,
|
||||
Textarea,
|
||||
JsonInput,
|
||||
Button,
|
||||
Group,
|
||||
Stack,
|
||||
Title,
|
||||
ActionIcon,
|
||||
ScrollArea,
|
||||
Box,
|
||||
Text,
|
||||
} from '@mantine/core';
|
||||
import { IconX } from '@tabler/icons-react';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { FormField, NotificationConfig } from '../../types';
|
||||
|
||||
export interface FormSidebarProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
title: string;
|
||||
editMode?: boolean;
|
||||
editItem?: any;
|
||||
fields: FormField[];
|
||||
onSubmit: (values: any) => Promise<void>;
|
||||
width?: number;
|
||||
initialValues?: Record<string, any>;
|
||||
validateOnSubmit?: boolean;
|
||||
}
|
||||
|
||||
const FormSidebar: React.FC<FormSidebarProps> = ({
|
||||
opened,
|
||||
onClose,
|
||||
onSuccess,
|
||||
title,
|
||||
editMode = false,
|
||||
editItem,
|
||||
fields,
|
||||
onSubmit,
|
||||
width = 450,
|
||||
initialValues = {},
|
||||
validateOnSubmit = true,
|
||||
}) => {
|
||||
const isEditing = editMode && !!editItem;
|
||||
|
||||
// Build initial form values from fields
|
||||
const buildInitialValues = () => {
|
||||
const values: Record<string, any> = {};
|
||||
fields.forEach(field => {
|
||||
values[field.name] = field.defaultValue ?? (field.type === 'multiselect' ? [] : '');
|
||||
});
|
||||
return { ...values, ...initialValues };
|
||||
};
|
||||
|
||||
// Build validation rules from fields
|
||||
const buildValidation = () => {
|
||||
const validation: Record<string, (value: any) => string | null> = {};
|
||||
fields.forEach(field => {
|
||||
validation[field.name] = (value: any) => {
|
||||
if (field.required && (!value || (typeof value === 'string' && value.trim() === ''))) {
|
||||
return `${field.label} is required`;
|
||||
}
|
||||
if (field.validation?.email && value && !/^\S+@\S+$/.test(value)) {
|
||||
return 'Invalid email format';
|
||||
}
|
||||
if (field.validation?.url && value && !/^https?:\/\/.+/.test(value)) {
|
||||
return 'Invalid URL format';
|
||||
}
|
||||
if (field.validation?.minLength && value && value.length < field.validation.minLength) {
|
||||
return `${field.label} must be at least ${field.validation.minLength} characters`;
|
||||
}
|
||||
if (field.validation?.maxLength && value && value.length > field.validation.maxLength) {
|
||||
return `${field.label} must be no more than ${field.validation.maxLength} characters`;
|
||||
}
|
||||
if (field.validation?.pattern && value && !field.validation.pattern.test(value)) {
|
||||
return `${field.label} format is invalid`;
|
||||
}
|
||||
if (field.validation?.custom) {
|
||||
return field.validation.custom(value);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
});
|
||||
return validation;
|
||||
};
|
||||
|
||||
const form = useForm({
|
||||
initialValues: buildInitialValues(),
|
||||
validate: buildValidation(),
|
||||
});
|
||||
|
||||
// Update form values when editItem changes
|
||||
useEffect(() => {
|
||||
if (isEditing && editItem) {
|
||||
const updatedValues: Record<string, any> = {};
|
||||
fields.forEach(field => {
|
||||
updatedValues[field.name] = editItem[field.name] ?? field.defaultValue ?? '';
|
||||
});
|
||||
form.setValues(updatedValues);
|
||||
} else if (!isEditing) {
|
||||
form.setValues(buildInitialValues());
|
||||
}
|
||||
}, [editItem, opened, isEditing]);
|
||||
|
||||
const handleSubmit = async (values: typeof form.values) => {
|
||||
try {
|
||||
await onSubmit(values);
|
||||
|
||||
const successNotification: NotificationConfig = {
|
||||
title: 'Success',
|
||||
message: `${title} ${isEditing ? 'updated' : 'created'} successfully`,
|
||||
color: 'green',
|
||||
};
|
||||
|
||||
notifications.show(successNotification);
|
||||
onSuccess();
|
||||
onClose();
|
||||
form.reset();
|
||||
} catch (error: any) {
|
||||
console.error(`Error ${isEditing ? 'updating' : 'creating'} ${title.toLowerCase()}:`, error);
|
||||
|
||||
const errorNotification: NotificationConfig = {
|
||||
title: 'Error',
|
||||
message: error.message || `Failed to ${isEditing ? 'update' : 'create'} ${title.toLowerCase()}`,
|
||||
color: 'red',
|
||||
};
|
||||
|
||||
notifications.show(errorNotification);
|
||||
}
|
||||
};
|
||||
|
||||
const renderField = (field: FormField) => {
|
||||
const inputProps = form.getInputProps(field.name);
|
||||
const commonProps = {
|
||||
key: field.name,
|
||||
label: field.label,
|
||||
placeholder: field.placeholder,
|
||||
description: field.description,
|
||||
required: field.required,
|
||||
disabled: field.disabled || (isEditing && field.name === 'id'),
|
||||
...inputProps,
|
||||
};
|
||||
|
||||
switch (field.type) {
|
||||
case 'email':
|
||||
return <TextInput {...commonProps} type="email" />;
|
||||
|
||||
case 'number':
|
||||
return <NumberInput {...commonProps} />;
|
||||
|
||||
case 'textarea':
|
||||
return <Textarea {...commonProps} autosize minRows={3} maxRows={6} />;
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<Select
|
||||
{...commonProps}
|
||||
data={field.options || []}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'multiselect':
|
||||
return (
|
||||
<MultiSelect
|
||||
{...commonProps}
|
||||
data={field.options || []}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'json':
|
||||
return (
|
||||
<JsonInput
|
||||
{...commonProps}
|
||||
validationError="Invalid JSON format"
|
||||
formatOnBlur
|
||||
autosize
|
||||
minRows={3}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return <TextInput {...commonProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 60,
|
||||
right: opened ? 0 : `-${width}px`,
|
||||
bottom: 0,
|
||||
width: `${width}px`,
|
||||
zIndex: 1000,
|
||||
borderRadius: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderLeft: '1px solid var(--mantine-color-gray-3)',
|
||||
backgroundColor: 'var(--mantine-color-body)',
|
||||
transition: 'right 0.3s ease',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Group justify="space-between" p="md" style={{ borderBottom: '1px solid var(--mantine-color-gray-3)' }}>
|
||||
<Title order={4}>
|
||||
{isEditing ? `Edit ${title}` : `Create New ${title}`}
|
||||
</Title>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={onClose}
|
||||
>
|
||||
<IconX size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
|
||||
{/* Content */}
|
||||
<ScrollArea style={{ flex: 1 }}>
|
||||
<Box p="md">
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack gap="md">
|
||||
{fields.map(renderField)}
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button variant="light" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
{isEditing ? 'Update' : 'Create'} {title}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
</ScrollArea>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormSidebar;
|
||||
223
web-components/src/hooks/useApiService.ts
Normal file
223
web-components/src/hooks/useApiService.ts
Normal file
@ -0,0 +1,223 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import { ApiResponse, PaginatedResponse, FilterOptions } from '../types';
|
||||
|
||||
export interface ApiServiceConfig {
|
||||
baseURL: string;
|
||||
defaultHeaders?: Record<string, string>;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export interface UseApiServiceReturn<T> {
|
||||
data: T[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
client: AxiosInstance;
|
||||
|
||||
// CRUD operations
|
||||
getAll: (filters?: FilterOptions) => Promise<T[]>;
|
||||
getById: (id: string) => Promise<T>;
|
||||
create: (data: Partial<T>) => Promise<T>;
|
||||
update: (id: string, data: Partial<T>) => Promise<T>;
|
||||
delete: (id: string) => Promise<void>;
|
||||
|
||||
// Utility methods
|
||||
clearError: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useApiService = <T extends { id: string }>(
|
||||
config: ApiServiceConfig,
|
||||
endpoint: string
|
||||
): UseApiServiceReturn<T> => {
|
||||
const [data, setData] = useState<T[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
|
||||
// Create axios instance
|
||||
const client = axios.create({
|
||||
baseURL: config.baseURL,
|
||||
timeout: config.timeout || 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...config.defaultHeaders,
|
||||
},
|
||||
});
|
||||
|
||||
// Add request interceptor for common headers
|
||||
client.interceptors.request.use(
|
||||
(config) => {
|
||||
// Add user email header if available (common pattern in the codebase)
|
||||
const userEmail = 'admin@example.com'; // This could come from a context or config
|
||||
if (userEmail) {
|
||||
config.headers['X-User-Email'] = userEmail;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// Add response interceptor for error handling
|
||||
client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const errorMessage = error.response?.data?.message || error.message || 'An error occurred';
|
||||
setError(errorMessage);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const getAll = useCallback(async (filters: FilterOptions = {}): Promise<T[]> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
params.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const response = await client.get<PaginatedResponse<T> | ApiResponse<T[]>>(`${endpoint}?${params.toString()}`);
|
||||
|
||||
// Handle both paginated and simple array responses
|
||||
if ('data' in response.data && Array.isArray(response.data.data)) {
|
||||
// Paginated response
|
||||
const paginatedData = response.data as PaginatedResponse<T>;
|
||||
setData(paginatedData.data);
|
||||
setTotal(paginatedData.total);
|
||||
setHasMore(paginatedData.has_more || false);
|
||||
return paginatedData.data;
|
||||
} else if ('data' in response.data && Array.isArray(response.data.data)) {
|
||||
// Simple array response wrapped in ApiResponse
|
||||
const apiData = response.data as ApiResponse<T[]>;
|
||||
setData(apiData.data);
|
||||
setTotal(apiData.data.length);
|
||||
setHasMore(false);
|
||||
return apiData.data;
|
||||
} else if (Array.isArray(response.data)) {
|
||||
// Direct array response
|
||||
setData(response.data);
|
||||
setTotal(response.data.length);
|
||||
setHasMore(false);
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error('Invalid response format');
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.message || err.message || 'Failed to fetch data';
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [client, endpoint]);
|
||||
|
||||
const getById = useCallback(async (id: string): Promise<T> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await client.get<ApiResponse<T> | T>(`${endpoint}/${id}`);
|
||||
const item = 'data' in response.data ? response.data.data : response.data;
|
||||
return item;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.message || err.message || 'Failed to fetch item';
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [client, endpoint]);
|
||||
|
||||
const create = useCallback(async (itemData: Partial<T>): Promise<T> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await client.post<ApiResponse<T> | T>(endpoint, itemData);
|
||||
const newItem = 'data' in response.data ? response.data.data : response.data;
|
||||
|
||||
// Update local data
|
||||
setData(prev => [...prev, newItem]);
|
||||
setTotal(prev => prev + 1);
|
||||
|
||||
return newItem;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.message || err.message || 'Failed to create item';
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [client, endpoint]);
|
||||
|
||||
const update = useCallback(async (id: string, itemData: Partial<T>): Promise<T> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await client.put<ApiResponse<T> | T>(`${endpoint}/${id}`, itemData);
|
||||
const updatedItem = 'data' in response.data ? response.data.data : response.data;
|
||||
|
||||
// Update local data
|
||||
setData(prev => prev.map(item => item.id === id ? updatedItem : item));
|
||||
|
||||
return updatedItem;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.message || err.message || 'Failed to update item';
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [client, endpoint]);
|
||||
|
||||
const deleteItem = useCallback(async (id: string): Promise<void> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await client.delete(`${endpoint}/${id}`);
|
||||
|
||||
// Update local data
|
||||
setData(prev => prev.filter(item => item.id !== id));
|
||||
setTotal(prev => prev - 1);
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.message || err.message || 'Failed to delete item';
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [client, endpoint]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await getAll();
|
||||
}, [getAll]);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
total,
|
||||
hasMore,
|
||||
client,
|
||||
getAll,
|
||||
getById,
|
||||
create,
|
||||
update,
|
||||
delete: deleteItem,
|
||||
clearError,
|
||||
refresh,
|
||||
};
|
||||
};
|
||||
108
web-components/src/hooks/useDataFilter.ts
Normal file
108
web-components/src/hooks/useDataFilter.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { FilterOptions, ListItem } from '../types';
|
||||
|
||||
export interface UseDataFilterOptions {
|
||||
searchFields?: string[];
|
||||
defaultFilters?: FilterOptions;
|
||||
debounceMs?: number;
|
||||
}
|
||||
|
||||
export interface UseDataFilterReturn {
|
||||
filteredData: ListItem[];
|
||||
filters: FilterOptions;
|
||||
setFilter: (key: string, value: any) => void;
|
||||
clearFilters: () => void;
|
||||
resetFilters: () => void;
|
||||
searchTerm: string;
|
||||
setSearchTerm: (term: string) => void;
|
||||
}
|
||||
|
||||
export const useDataFilter = (
|
||||
data: ListItem[],
|
||||
options: UseDataFilterOptions = {}
|
||||
): UseDataFilterReturn => {
|
||||
const {
|
||||
searchFields = ['name', 'title', 'email', 'description'],
|
||||
defaultFilters = {},
|
||||
} = options;
|
||||
|
||||
const [filters, setFilters] = useState<FilterOptions>(defaultFilters);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const setFilter = useCallback((key: string, value: any) => {
|
||||
if (key === 'search') {
|
||||
setSearchTerm(value);
|
||||
setFilters(prev => ({ ...prev, search: value }));
|
||||
} else {
|
||||
setFilters(prev => ({ ...prev, [key]: value }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearFilters = useCallback(() => {
|
||||
setFilters({});
|
||||
setSearchTerm('');
|
||||
}, []);
|
||||
|
||||
const resetFilters = useCallback(() => {
|
||||
setFilters(defaultFilters);
|
||||
setSearchTerm('');
|
||||
}, [defaultFilters]);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
let result = [...data];
|
||||
|
||||
// Apply search filter
|
||||
if (searchTerm.trim()) {
|
||||
const term = searchTerm.toLowerCase().trim();
|
||||
result = result.filter(item => {
|
||||
return searchFields.some(field => {
|
||||
const value = item[field];
|
||||
if (!value) return false;
|
||||
return value.toString().toLowerCase().includes(term);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Apply other filters
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key === 'search') return; // Already handled above
|
||||
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
// Array filter (e.g., multiple status selection)
|
||||
result = result.filter(item => value.includes(item[key]));
|
||||
} else {
|
||||
// Single value filter
|
||||
result = result.filter(item => {
|
||||
const itemValue = item[key];
|
||||
if (itemValue == null) return false;
|
||||
|
||||
// Exact match for most cases
|
||||
if (itemValue.toString().toLowerCase() === value.toString().toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Partial match for string fields
|
||||
if (typeof itemValue === 'string' && typeof value === 'string') {
|
||||
return itemValue.toLowerCase().includes(value.toLowerCase());
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [data, searchTerm, filters, searchFields]);
|
||||
|
||||
return {
|
||||
filteredData,
|
||||
filters,
|
||||
setFilter,
|
||||
clearFilters,
|
||||
resetFilters,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
};
|
||||
};
|
||||
83
web-components/src/index.ts
Normal file
83
web-components/src/index.ts
Normal file
@ -0,0 +1,83 @@
|
||||
// Components
|
||||
export { default as FormSidebar } from './components/FormSidebar/FormSidebar';
|
||||
export { default as DataTable } from './components/DataTable/DataTable';
|
||||
|
||||
// Types
|
||||
export * from './types';
|
||||
|
||||
// Hooks
|
||||
export { useApiService } from './hooks/useApiService';
|
||||
export { useDataFilter } from './hooks/useDataFilter';
|
||||
|
||||
// Utils
|
||||
export * from './utils/notifications';
|
||||
export * from './utils/validation';
|
||||
|
||||
// Re-export common Mantine components and utilities for consistency
|
||||
export {
|
||||
// Core components that are commonly used
|
||||
Paper,
|
||||
Stack,
|
||||
Group,
|
||||
Button,
|
||||
TextInput,
|
||||
Select,
|
||||
MultiSelect,
|
||||
NumberInput,
|
||||
Textarea,
|
||||
JsonInput,
|
||||
ActionIcon,
|
||||
Menu,
|
||||
Text,
|
||||
Title,
|
||||
Badge,
|
||||
Table,
|
||||
Pagination,
|
||||
LoadingOverlay,
|
||||
Center,
|
||||
Box,
|
||||
ScrollArea,
|
||||
Divider,
|
||||
|
||||
|
||||
} from '@mantine/core';
|
||||
|
||||
// Re-export common hooks
|
||||
export {
|
||||
useDisclosure,
|
||||
useToggle,
|
||||
useLocalStorage,
|
||||
} from '@mantine/hooks';
|
||||
|
||||
// Re-export form utilities
|
||||
export { useForm } from '@mantine/form';
|
||||
|
||||
// Re-export notifications
|
||||
export { notifications } from '@mantine/notifications';
|
||||
|
||||
// Re-export modals
|
||||
export { modals } from '@mantine/modals';
|
||||
|
||||
// Re-export common icons
|
||||
export {
|
||||
IconPlus,
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
IconSearch,
|
||||
IconFilter,
|
||||
IconRefresh,
|
||||
IconX,
|
||||
IconDots,
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconUser,
|
||||
IconUsers,
|
||||
IconKey,
|
||||
IconSettings,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconCopy,
|
||||
IconCheck,
|
||||
IconAlertCircle,
|
||||
IconInfoCircle,
|
||||
} from '@tabler/icons-react';
|
||||
87
web-components/src/types/index.ts
Normal file
87
web-components/src/types/index.ts
Normal file
@ -0,0 +1,87 @@
|
||||
// Common types used across all microfrontends
|
||||
|
||||
export interface BaseEntity {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export interface Owner {
|
||||
type: 'individual' | 'team';
|
||||
name: string;
|
||||
owner: string;
|
||||
}
|
||||
|
||||
export interface FormSidebarProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
editItem?: any;
|
||||
}
|
||||
|
||||
export interface ListItem {
|
||||
id: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
email?: string;
|
||||
status?: string;
|
||||
role?: string;
|
||||
type?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
export interface FilterOptions {
|
||||
search?: string;
|
||||
status?: string;
|
||||
type?: string;
|
||||
role?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface NotificationConfig {
|
||||
title: string;
|
||||
message: string;
|
||||
color: 'red' | 'green' | 'blue' | 'yellow' | 'gray';
|
||||
[key: string]: any; // Allow additional properties for Mantine compatibility
|
||||
}
|
||||
|
||||
export interface ValidationRule {
|
||||
required?: boolean;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
pattern?: RegExp;
|
||||
email?: boolean;
|
||||
url?: boolean;
|
||||
custom?: (value: any) => string | null;
|
||||
}
|
||||
|
||||
export interface FormField {
|
||||
name: string;
|
||||
label: string;
|
||||
type: 'text' | 'email' | 'number' | 'select' | 'multiselect' | 'textarea' | 'date' | 'json';
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
options?: Array<{ value: string; label: string }>;
|
||||
validation?: ValidationRule;
|
||||
defaultValue?: any;
|
||||
}
|
||||
95
web-components/src/utils/notifications.ts
Normal file
95
web-components/src/utils/notifications.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { NotificationConfig } from '../types';
|
||||
|
||||
export const showSuccessNotification = (message: string, title = 'Success') => {
|
||||
const config: NotificationConfig = {
|
||||
title,
|
||||
message,
|
||||
color: 'green',
|
||||
};
|
||||
notifications.show(config);
|
||||
};
|
||||
|
||||
export const showErrorNotification = (message: string, title = 'Error') => {
|
||||
const config: NotificationConfig = {
|
||||
title,
|
||||
message,
|
||||
color: 'red',
|
||||
};
|
||||
notifications.show(config);
|
||||
};
|
||||
|
||||
export const showWarningNotification = (message: string, title = 'Warning') => {
|
||||
const config: NotificationConfig = {
|
||||
title,
|
||||
message,
|
||||
color: 'yellow',
|
||||
};
|
||||
notifications.show(config);
|
||||
};
|
||||
|
||||
export const showInfoNotification = (message: string, title = 'Info') => {
|
||||
const config: NotificationConfig = {
|
||||
title,
|
||||
message,
|
||||
color: 'blue',
|
||||
};
|
||||
notifications.show(config);
|
||||
};
|
||||
|
||||
// Common notification messages used across microfrontends
|
||||
export const NotificationMessages = {
|
||||
// Generic CRUD operations
|
||||
createSuccess: (entityName: string) => `${entityName} created successfully`,
|
||||
updateSuccess: (entityName: string) => `${entityName} updated successfully`,
|
||||
deleteSuccess: (entityName: string) => `${entityName} deleted successfully`,
|
||||
|
||||
createError: (entityName: string) => `Failed to create ${entityName}`,
|
||||
updateError: (entityName: string) => `Failed to update ${entityName}`,
|
||||
deleteError: (entityName: string) => `Failed to delete ${entityName}`,
|
||||
|
||||
loadError: (entityName: string) => `Failed to load ${entityName}`,
|
||||
networkError: 'Network error occurred. Please try again.',
|
||||
|
||||
// Validation
|
||||
validationError: 'Please check the form for errors',
|
||||
requiredFieldError: (fieldName: string) => `${fieldName} is required`,
|
||||
|
||||
// Authentication
|
||||
authRequired: 'Authentication required',
|
||||
permissionDenied: 'Permission denied',
|
||||
sessionExpired: 'Session expired. Please log in again.',
|
||||
|
||||
// Application-specific
|
||||
applicationCreated: 'Application created successfully',
|
||||
applicationUpdated: 'Application updated successfully',
|
||||
applicationDeleted: 'Application deleted successfully',
|
||||
|
||||
tokenCreated: 'Token created successfully',
|
||||
tokenRevoked: 'Token revoked successfully',
|
||||
|
||||
userCreated: 'User created successfully',
|
||||
userUpdated: 'User updated successfully',
|
||||
userDeleted: 'User deleted successfully',
|
||||
|
||||
functionCreated: 'Function created successfully',
|
||||
functionUpdated: 'Function updated successfully',
|
||||
functionDeleted: 'Function deleted successfully',
|
||||
|
||||
executionStarted: 'Function execution started',
|
||||
executionCompleted: 'Function execution completed',
|
||||
executionFailed: 'Function execution failed',
|
||||
};
|
||||
|
||||
// Utility to show notifications for common operations
|
||||
export const showCrudNotification = {
|
||||
success: (operation: 'create' | 'update' | 'delete', entityName: string) => {
|
||||
const message = NotificationMessages[`${operation}Success`](entityName);
|
||||
showSuccessNotification(message);
|
||||
},
|
||||
|
||||
error: (operation: 'create' | 'update' | 'delete' | 'load', entityName: string, customMessage?: string) => {
|
||||
const message = customMessage || NotificationMessages[`${operation}Error`](entityName);
|
||||
showErrorNotification(message);
|
||||
},
|
||||
};
|
||||
137
web-components/src/utils/validation.ts
Normal file
137
web-components/src/utils/validation.ts
Normal file
@ -0,0 +1,137 @@
|
||||
// Common validation patterns used across microfrontends
|
||||
|
||||
export const ValidationPatterns = {
|
||||
email: /^\S+@\S+\.\S+$/,
|
||||
url: /^https?:\/\/.+/,
|
||||
duration: /^\d+[smhd]$/, // e.g., "30s", "5m", "2h", "1d"
|
||||
token: /^[a-zA-Z0-9_-]+$/,
|
||||
appId: /^[a-zA-Z0-9-_]+$/,
|
||||
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||
};
|
||||
|
||||
export const ValidationMessages = {
|
||||
required: (fieldName: string) => `${fieldName} is required`,
|
||||
email: 'Please enter a valid email address',
|
||||
url: 'Please enter a valid URL (http:// or https://)',
|
||||
duration: 'Duration must be in format like 30s, 5m, 2h, 1d',
|
||||
minLength: (fieldName: string, minLength: number) =>
|
||||
`${fieldName} must be at least ${minLength} characters`,
|
||||
maxLength: (fieldName: string, maxLength: number) =>
|
||||
`${fieldName} must be no more than ${maxLength} characters`,
|
||||
pattern: (fieldName: string) => `${fieldName} format is invalid`,
|
||||
token: 'Token can only contain letters, numbers, underscores and hyphens',
|
||||
appId: 'App ID can only contain letters, numbers, hyphens and underscores',
|
||||
uuid: 'Please enter a valid UUID',
|
||||
positiveNumber: 'Must be a positive number',
|
||||
range: (fieldName: string, min: number, max: number) =>
|
||||
`${fieldName} must be between ${min} and ${max}`,
|
||||
};
|
||||
|
||||
// Common validation functions
|
||||
export const validateRequired = (value: any): string | null => {
|
||||
if (value === null || value === undefined || value === '' ||
|
||||
(Array.isArray(value) && value.length === 0)) {
|
||||
return 'This field is required';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const validateEmail = (value: string): string | null => {
|
||||
if (!value) return null;
|
||||
return ValidationPatterns.email.test(value) ? null : ValidationMessages.email;
|
||||
};
|
||||
|
||||
export const validateUrl = (value: string): string | null => {
|
||||
if (!value) return null;
|
||||
return ValidationPatterns.url.test(value) ? null : ValidationMessages.url;
|
||||
};
|
||||
|
||||
export const validateDuration = (value: string): string | null => {
|
||||
if (!value) return null;
|
||||
return ValidationPatterns.duration.test(value) ? null : ValidationMessages.duration;
|
||||
};
|
||||
|
||||
export const validateMinLength = (value: string, minLength: number, fieldName = 'Field'): string | null => {
|
||||
if (!value) return null;
|
||||
return value.length >= minLength ? null : ValidationMessages.minLength(fieldName, minLength);
|
||||
};
|
||||
|
||||
export const validateMaxLength = (value: string, maxLength: number, fieldName = 'Field'): string | null => {
|
||||
if (!value) return null;
|
||||
return value.length <= maxLength ? null : ValidationMessages.maxLength(fieldName, maxLength);
|
||||
};
|
||||
|
||||
export const validatePattern = (value: string, pattern: RegExp, fieldName = 'Field'): string | null => {
|
||||
if (!value) return null;
|
||||
return pattern.test(value) ? null : ValidationMessages.pattern(fieldName);
|
||||
};
|
||||
|
||||
export const validateRange = (value: number, min: number, max: number, fieldName = 'Field'): string | null => {
|
||||
if (value == null) return null;
|
||||
if (value < min || value > max) {
|
||||
return ValidationMessages.range(fieldName, min, max);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const validateAppId = (value: string): string | null => {
|
||||
if (!value) return null;
|
||||
return ValidationPatterns.appId.test(value) ? null : ValidationMessages.appId;
|
||||
};
|
||||
|
||||
export const validateToken = (value: string): string | null => {
|
||||
if (!value) return null;
|
||||
return ValidationPatterns.token.test(value) ? null : ValidationMessages.token;
|
||||
};
|
||||
|
||||
export const validateUuid = (value: string): string | null => {
|
||||
if (!value) return null;
|
||||
return ValidationPatterns.uuid.test(value) ? null : ValidationMessages.uuid;
|
||||
};
|
||||
|
||||
export const validateJsonString = (value: string): string | null => {
|
||||
if (!value || value.trim() === '') return null;
|
||||
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return null;
|
||||
} catch (error) {
|
||||
return 'Invalid JSON format';
|
||||
}
|
||||
};
|
||||
|
||||
// Utility function to parse duration strings to seconds (common across KMS and FaaS)
|
||||
export const parseDuration = (duration: string): number => {
|
||||
const match = duration.match(/^(\d+)([smhd]?)$/);
|
||||
if (!match) return 86400; // Default to 24h in seconds
|
||||
|
||||
const value = parseInt(match[1]);
|
||||
const unit = match[2] || 'h';
|
||||
|
||||
switch (unit) {
|
||||
case 's': return value; // seconds
|
||||
case 'm': return value * 60; // minutes to seconds
|
||||
case 'h': return value * 3600; // hours to seconds
|
||||
case 'd': return value * 86400; // days to seconds
|
||||
default: return value * 3600; // default to hours
|
||||
}
|
||||
};
|
||||
|
||||
// Utility function to format duration from seconds to human readable
|
||||
export const formatDuration = (seconds: number): string => {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
||||
return `${Math.floor(seconds / 86400)}d`;
|
||||
};
|
||||
|
||||
// Combine multiple validators
|
||||
export const combineValidators = (...validators: Array<(value: any) => string | null>) => {
|
||||
return (value: any): string | null => {
|
||||
for (const validator of validators) {
|
||||
const error = validator(value);
|
||||
if (error) return error;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
};
|
||||
37
web-components/tsconfig.json
Normal file
37
web-components/tsconfig.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"declarationDir": "dist",
|
||||
"outDir": "dist",
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.stories.ts",
|
||||
"**/*.stories.tsx"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user