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