This commit is contained in:
2025-08-31 23:27:52 -04:00
parent 23dfc171b8
commit 40f8780dec
27 changed files with 24228 additions and 0 deletions

21785
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View 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"
}
}

View 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
View 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

View 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;

View 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;

View 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>;

View 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
View 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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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
View 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;
}

View 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;
};

View 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;

View 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"
}

View 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',
],
};

View 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;

View 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;

View 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,
};
};

View 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,
};
};

View 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';

View 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;
}

View 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);
},
};

View 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;
};
};

View 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"
]
}