-
This commit is contained in:
@ -11,7 +11,8 @@
|
||||
"@mantine/form": "^7.0.0",
|
||||
"@mantine/modals": "^7.0.0",
|
||||
"@tabler/icons-react": "^2.40.0",
|
||||
"axios": "^1.6.0"
|
||||
"axios": "^1.6.0",
|
||||
"@skybridge/web-components": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.0",
|
||||
|
||||
@ -12,6 +12,132 @@ npm install @skybridge/web-components
|
||||
|
||||
## Components
|
||||
|
||||
### StatusBadge
|
||||
|
||||
A standardized badge component with consistent color schemes across all microfrontends.
|
||||
|
||||
```tsx
|
||||
import { StatusBadge, UserRoleBadge, RuntimeBadge } from '@skybridge/web-components';
|
||||
|
||||
// Generic usage
|
||||
<StatusBadge value="active" variant="status" />
|
||||
|
||||
// Context-specific usage
|
||||
<UserRoleBadge value="admin" />
|
||||
<RuntimeBadge value="nodejs18" />
|
||||
|
||||
// Variants: 'status', 'role', 'runtime', 'type', 'severity', 'execution'
|
||||
```
|
||||
|
||||
### EmptyState
|
||||
|
||||
A comprehensive empty state component for consistent empty data handling.
|
||||
|
||||
```tsx
|
||||
import { EmptyState, NoUsersState, NoSearchResults } from '@skybridge/web-components';
|
||||
|
||||
// Generic empty state
|
||||
<EmptyState
|
||||
variant="no-data"
|
||||
context="users"
|
||||
onAdd={() => handleAddUser()}
|
||||
onRefresh={() => handleRefresh()}
|
||||
/>
|
||||
|
||||
// Convenience components
|
||||
<NoUsersState onAddUser={handleAddUser} />
|
||||
<NoSearchResults onClearFilters={handleClear} onRefresh={handleRefresh} />
|
||||
```
|
||||
|
||||
### Sidebar
|
||||
|
||||
A flexible base sidebar component for consistent slide-out panels.
|
||||
|
||||
```tsx
|
||||
import { Sidebar, DetailsSidebar, useSidebar } from '@skybridge/web-components';
|
||||
|
||||
const MySidebar = () => {
|
||||
const { opened, open, close } = useSidebar();
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title="My Panel"
|
||||
width={400}
|
||||
>
|
||||
<p>Content goes here</p>
|
||||
</Sidebar>
|
||||
);
|
||||
};
|
||||
|
||||
// For item details
|
||||
<DetailsSidebar
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
itemName="John Doe"
|
||||
itemType="User"
|
||||
status={<StatusBadge value="active" />}
|
||||
editButton={<Button onClick={handleEdit}>Edit</Button>}
|
||||
>
|
||||
<UserDetails user={selectedUser} />
|
||||
</DetailsSidebar>
|
||||
```
|
||||
|
||||
### ActionMenu
|
||||
|
||||
A standardized action menu component for table rows and items.
|
||||
|
||||
```tsx
|
||||
import { ActionMenu, getUserActions, createEditAction } from '@skybridge/web-components';
|
||||
|
||||
// Using pre-built action sets
|
||||
<ActionMenu
|
||||
item={user}
|
||||
actions={getUserActions(handleEdit, handleDelete, handleViewDetails)}
|
||||
/>
|
||||
|
||||
// Custom actions
|
||||
<ActionMenu
|
||||
item={item}
|
||||
actions={[
|
||||
createEditAction(handleEdit),
|
||||
createDeleteAction(handleDelete, 'user'),
|
||||
{
|
||||
key: 'custom',
|
||||
label: 'Custom Action',
|
||||
icon: IconSettings,
|
||||
onClick: handleCustomAction,
|
||||
}
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### LoadingState
|
||||
|
||||
Comprehensive loading states for different scenarios.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
LoadingState,
|
||||
TableLoadingState,
|
||||
PageLoadingState,
|
||||
useLoadingState
|
||||
} from '@skybridge/web-components';
|
||||
|
||||
// Different loading variants
|
||||
<LoadingState variant="spinner" message="Loading data..." />
|
||||
<LoadingState variant="progress" progress={75} progressLabel="3/4 complete" />
|
||||
<LoadingState variant="skeleton-table" rows={5} columns={3} />
|
||||
|
||||
// Convenience components
|
||||
<TableLoadingState rows={10} />
|
||||
<PageLoadingState message="Loading application..." />
|
||||
|
||||
// Hook for loading state management
|
||||
const { loading, startLoading, stopLoading, updateProgress } = useLoadingState();
|
||||
```
|
||||
|
||||
### FormSidebar
|
||||
|
||||
A reusable sidebar form component that handles create/edit operations with validation.
|
||||
@ -288,14 +414,18 @@ The component library is designed to:
|
||||
|
||||
## Common Patterns Extracted
|
||||
|
||||
Based on analysis of the existing microfrontends, this library extracts these common patterns:
|
||||
Based on deep 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
|
||||
1. **Status Display**: Standardized color schemes and formatting for status badges across all apps
|
||||
2. **Empty States**: Consistent empty data handling with contextual actions and messaging
|
||||
3. **Sidebar Components**: All microfrontends use similar slide-out forms and detail panels
|
||||
4. **Action Menus**: Standardized table actions with confirmation dialogs and consistent UX
|
||||
5. **Loading States**: Multiple loading variants (spinners, progress, skeletons) for different scenarios
|
||||
6. **Form Handling**: Reusable form sidebars with validation and error handling
|
||||
7. **Data Tables**: Consistent table layouts with actions, filtering, and pagination
|
||||
8. **API Integration**: Standard CRUD operations with error handling and state management
|
||||
9. **Validation**: Common validation rules and patterns with consistent error messages
|
||||
10. **Notifications**: Standardized success/error messaging and CRUD operation feedback
|
||||
|
||||
## Compatibility
|
||||
|
||||
|
||||
43
web-components/dist/components/ActionMenu/ActionMenu.d.ts
vendored
Normal file
43
web-components/dist/components/ActionMenu/ActionMenu.d.ts
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { TablerIconsProps } from '@tabler/icons-react';
|
||||
export interface ActionMenuItem {
|
||||
key: string;
|
||||
label: string;
|
||||
icon?: React.ComponentType<TablerIconsProps>;
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
onClick: (item?: any) => void | Promise<void>;
|
||||
confirm?: {
|
||||
title: string;
|
||||
message: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
};
|
||||
show?: (item: any) => boolean;
|
||||
}
|
||||
export interface ActionMenuProps {
|
||||
item?: any;
|
||||
actions: ActionMenuItem[];
|
||||
trigger?: 'dots' | 'button' | 'custom';
|
||||
triggerLabel?: string;
|
||||
triggerIcon?: React.ComponentType<TablerIconsProps>;
|
||||
triggerProps?: any;
|
||||
customTrigger?: React.ReactNode;
|
||||
position?: 'bottom-end' | 'bottom-start' | 'top-end' | 'top-start';
|
||||
withArrow?: boolean;
|
||||
withinPortal?: boolean;
|
||||
'aria-label'?: string;
|
||||
}
|
||||
declare const ActionMenu: React.FC<ActionMenuProps>;
|
||||
export default ActionMenu;
|
||||
export declare const createViewAction: (onView: (item: any) => void) => ActionMenuItem;
|
||||
export declare const createEditAction: (onEdit: (item: any) => void) => ActionMenuItem;
|
||||
export declare const createCopyAction: (onCopy: (item: any) => void) => ActionMenuItem;
|
||||
export declare const createDeleteAction: (onDelete: (item: any) => void | Promise<void>, itemName?: string) => ActionMenuItem;
|
||||
export declare const createArchiveAction: (onArchive: (item: any) => void) => ActionMenuItem;
|
||||
export declare const createRestoreAction: (onRestore: (item: any) => void) => ActionMenuItem;
|
||||
export declare const getUserActions: (onEdit: (item: any) => void, onDelete: (item: any) => void, onViewDetails?: (item: any) => void) => ActionMenuItem[];
|
||||
export declare const getApplicationActions: (onEdit: (item: any) => void, onDelete: (item: any) => void, onConfigure?: (item: any) => void) => ActionMenuItem[];
|
||||
export declare const getFunctionActions: (onEdit: (item: any) => void, onDelete: (item: any) => void, onExecute?: (item: any) => void, onViewLogs?: (item: any) => void) => ActionMenuItem[];
|
||||
export declare const getTokenActions: (onRevoke: (item: any) => void, onCopy?: (item: any) => void, onRefresh?: (item: any) => void) => ActionMenuItem[];
|
||||
47
web-components/dist/components/EmptyState/EmptyState.d.ts
vendored
Normal file
47
web-components/dist/components/EmptyState/EmptyState.d.ts
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { TablerIconsProps } from '@tabler/icons-react';
|
||||
export type EmptyStateVariant = 'no-data' | 'no-results' | 'error' | 'loading-failed' | 'access-denied' | 'coming-soon';
|
||||
export type EmptyStateContext = 'users' | 'applications' | 'functions' | 'tokens' | 'executions' | 'permissions' | 'audit' | 'generic';
|
||||
export interface EmptyStateAction {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
variant?: 'filled' | 'light' | 'outline';
|
||||
color?: string;
|
||||
leftSection?: React.ReactNode;
|
||||
}
|
||||
export interface EmptyStateProps {
|
||||
variant?: EmptyStateVariant;
|
||||
context?: EmptyStateContext;
|
||||
title?: string;
|
||||
message?: string;
|
||||
icon?: React.ComponentType<TablerIconsProps>;
|
||||
iconSize?: number;
|
||||
iconColor?: string;
|
||||
actions?: EmptyStateAction[];
|
||||
height?: number | string;
|
||||
}
|
||||
declare const EmptyState: React.FC<EmptyStateProps & {
|
||||
onAdd?: () => void;
|
||||
onRefresh?: () => void;
|
||||
onClearFilters?: () => void;
|
||||
}>;
|
||||
export default EmptyState;
|
||||
export declare const NoUsersState: React.FC<Omit<EmptyStateProps, 'context' | 'variant'> & {
|
||||
onAddUser?: () => void;
|
||||
}>;
|
||||
export declare const NoApplicationsState: React.FC<Omit<EmptyStateProps, 'context' | 'variant'> & {
|
||||
onCreateApp?: () => void;
|
||||
}>;
|
||||
export declare const NoFunctionsState: React.FC<Omit<EmptyStateProps, 'context' | 'variant'> & {
|
||||
onCreateFunction?: () => void;
|
||||
}>;
|
||||
export declare const NoTokensState: React.FC<Omit<EmptyStateProps, 'context' | 'variant'> & {
|
||||
onGenerateToken?: () => void;
|
||||
}>;
|
||||
export declare const NoSearchResults: React.FC<Omit<EmptyStateProps, 'variant'> & {
|
||||
onClearFilters?: () => void;
|
||||
onRefresh?: () => void;
|
||||
}>;
|
||||
export declare const ErrorState: React.FC<Omit<EmptyStateProps, 'variant'> & {
|
||||
onRetry?: () => void;
|
||||
}>;
|
||||
46
web-components/dist/components/LoadingState/LoadingState.d.ts
vendored
Normal file
46
web-components/dist/components/LoadingState/LoadingState.d.ts
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
export type LoadingVariant = 'spinner' | 'progress' | 'skeleton-table' | 'skeleton-cards' | 'skeleton-form' | 'skeleton-text' | 'dots' | 'overlay';
|
||||
export type LoadingSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
export interface LoadingStateProps {
|
||||
variant?: LoadingVariant;
|
||||
size?: LoadingSize;
|
||||
height?: number | string;
|
||||
message?: string;
|
||||
submessage?: string;
|
||||
progress?: number;
|
||||
progressLabel?: string;
|
||||
rows?: number;
|
||||
columns?: number;
|
||||
color?: string;
|
||||
withContainer?: boolean;
|
||||
animate?: boolean;
|
||||
}
|
||||
declare const LoadingState: React.FC<LoadingStateProps>;
|
||||
export default LoadingState;
|
||||
export declare const TableLoadingState: React.FC<{
|
||||
rows?: number;
|
||||
columns?: number;
|
||||
}>;
|
||||
export declare const CardsLoadingState: React.FC<{
|
||||
count?: number;
|
||||
columns?: number;
|
||||
}>;
|
||||
export declare const FormLoadingState: React.FC<{
|
||||
fields?: number;
|
||||
}>;
|
||||
export declare const PageLoadingState: React.FC<{
|
||||
message?: string;
|
||||
}>;
|
||||
export declare const InlineLoadingState: React.FC<{
|
||||
message?: string;
|
||||
size?: LoadingSize;
|
||||
}>;
|
||||
export declare const useLoadingState: (initialLoading?: boolean) => {
|
||||
loading: boolean;
|
||||
progress: number;
|
||||
startLoading: () => void;
|
||||
stopLoading: () => void;
|
||||
updateProgress: (value: number) => void;
|
||||
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setProgress: React.Dispatch<React.SetStateAction<number>>;
|
||||
};
|
||||
49
web-components/dist/components/Sidebar/Sidebar.d.ts
vendored
Normal file
49
web-components/dist/components/Sidebar/Sidebar.d.ts
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
export interface SidebarProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
width?: number;
|
||||
position?: 'left' | 'right';
|
||||
headerActions?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
zIndex?: number;
|
||||
offsetTop?: number;
|
||||
backgroundColor?: string;
|
||||
borderColor?: string;
|
||||
animationDuration?: string;
|
||||
'aria-label'?: string;
|
||||
}
|
||||
declare const Sidebar: React.FC<SidebarProps>;
|
||||
export default Sidebar;
|
||||
export interface FormSidebarWrapperProps extends Omit<SidebarProps, 'children'> {
|
||||
children: React.ReactNode;
|
||||
cancelLabel?: string;
|
||||
submitLabel?: string;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: () => void;
|
||||
submitDisabled?: boolean;
|
||||
showFooterActions?: boolean;
|
||||
}
|
||||
export declare const FormSidebarWrapper: React.FC<FormSidebarWrapperProps>;
|
||||
export interface DetailsSidebarProps extends Omit<SidebarProps, 'title'> {
|
||||
itemName: string;
|
||||
itemType?: string;
|
||||
editButton?: React.ReactNode;
|
||||
deleteButton?: React.ReactNode;
|
||||
status?: React.ReactNode;
|
||||
}
|
||||
export declare const DetailsSidebar: React.FC<DetailsSidebarProps>;
|
||||
export interface QuickSidebarProps extends Omit<SidebarProps, 'children'> {
|
||||
content: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
export declare const QuickSidebar: React.FC<QuickSidebarProps>;
|
||||
export declare const useSidebar: (initialOpened?: boolean) => {
|
||||
opened: boolean;
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
toggle: () => void;
|
||||
setOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
18
web-components/dist/components/StatusBadge/StatusBadge.d.ts
vendored
Normal file
18
web-components/dist/components/StatusBadge/StatusBadge.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { BadgeProps } from '@mantine/core';
|
||||
export type StatusVariant = 'status' | 'role' | 'runtime' | 'type' | 'severity' | 'execution';
|
||||
export interface StatusBadgeProps extends Omit<BadgeProps, 'color' | 'children'> {
|
||||
value: string;
|
||||
variant?: StatusVariant;
|
||||
customColorMap?: Record<string, string>;
|
||||
}
|
||||
declare const COLOR_MAPS: Record<StatusVariant, Record<string, string>>;
|
||||
declare const DEFAULT_COLORS: Record<StatusVariant, string>;
|
||||
declare const StatusBadge: React.FC<StatusBadgeProps>;
|
||||
export default StatusBadge;
|
||||
export { COLOR_MAPS, DEFAULT_COLORS };
|
||||
export declare const UserRoleBadge: React.FC<Omit<StatusBadgeProps, 'variant'>>;
|
||||
export declare const ApplicationTypeBadge: React.FC<Omit<StatusBadgeProps, 'variant'>>;
|
||||
export declare const RuntimeBadge: React.FC<Omit<StatusBadgeProps, 'variant'>>;
|
||||
export declare const ExecutionStatusBadge: React.FC<Omit<StatusBadgeProps, 'variant'>>;
|
||||
export declare const SeverityBadge: React.FC<Omit<StatusBadgeProps, 'variant'>>;
|
||||
5
web-components/dist/index.d.ts
vendored
5
web-components/dist/index.d.ts
vendored
@ -1,5 +1,10 @@
|
||||
export { default as FormSidebar } from './components/FormSidebar/FormSidebar';
|
||||
export { default as DataTable } from './components/DataTable/DataTable';
|
||||
export { default as StatusBadge, UserRoleBadge, ApplicationTypeBadge, RuntimeBadge, ExecutionStatusBadge, SeverityBadge } from './components/StatusBadge/StatusBadge';
|
||||
export { default as EmptyState, NoUsersState, NoApplicationsState, NoFunctionsState, NoTokensState, NoSearchResults, ErrorState } from './components/EmptyState/EmptyState';
|
||||
export { default as Sidebar, FormSidebarWrapper, DetailsSidebar, QuickSidebar, useSidebar } from './components/Sidebar/Sidebar';
|
||||
export { default as ActionMenu, createViewAction, createEditAction, createCopyAction, createDeleteAction, createArchiveAction, createRestoreAction, getUserActions, getApplicationActions, getFunctionActions, getTokenActions } from './components/ActionMenu/ActionMenu';
|
||||
export { default as LoadingState, TableLoadingState, CardsLoadingState, FormLoadingState, PageLoadingState, InlineLoadingState, useLoadingState } from './components/LoadingState/LoadingState';
|
||||
export * from './types';
|
||||
export { useApiService } from './hooks/useApiService';
|
||||
export { useDataFilter } from './hooks/useDataFilter';
|
||||
|
||||
2
web-components/dist/index.esm.js
vendored
2
web-components/dist/index.esm.js
vendored
File diff suppressed because one or more lines are too long
2
web-components/dist/index.esm.js.map
vendored
2
web-components/dist/index.esm.js.map
vendored
File diff suppressed because one or more lines are too long
2
web-components/dist/index.js
vendored
2
web-components/dist/index.js
vendored
File diff suppressed because one or more lines are too long
2
web-components/dist/index.js.map
vendored
2
web-components/dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
5
web-components/dist/types/index.d.ts
vendored
5
web-components/dist/types/index.d.ts
vendored
@ -77,3 +77,8 @@ export interface FormField {
|
||||
validation?: ValidationRule;
|
||||
defaultValue?: any;
|
||||
}
|
||||
export type { StatusVariant, StatusBadgeProps } from '../components/StatusBadge/StatusBadge';
|
||||
export type { EmptyStateVariant, EmptyStateContext, EmptyStateProps, EmptyStateAction } from '../components/EmptyState/EmptyState';
|
||||
export type { SidebarProps, FormSidebarWrapperProps, DetailsSidebarProps, QuickSidebarProps } from '../components/Sidebar/Sidebar';
|
||||
export type { ActionMenuItem, ActionMenuProps } from '../components/ActionMenu/ActionMenu';
|
||||
export type { LoadingVariant, LoadingSize, LoadingStateProps } from '../components/LoadingState/LoadingState';
|
||||
|
||||
374
web-components/src/components/ActionMenu/ActionMenu.tsx
Normal file
374
web-components/src/components/ActionMenu/ActionMenu.tsx
Normal file
@ -0,0 +1,374 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Menu,
|
||||
ActionIcon,
|
||||
Button,
|
||||
Group,
|
||||
Text,
|
||||
Divider,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconDots,
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
IconEye,
|
||||
IconCopy,
|
||||
IconDownload,
|
||||
IconShare,
|
||||
IconArchive,
|
||||
IconRestore,
|
||||
IconSettings,
|
||||
IconPlayerPlay,
|
||||
IconPlayerPause,
|
||||
IconPlayerStop,
|
||||
IconRefresh,
|
||||
TablerIconsProps,
|
||||
} from '@tabler/icons-react';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
||||
export interface ActionMenuItem {
|
||||
key: string;
|
||||
label: string;
|
||||
icon?: React.ComponentType<TablerIconsProps>;
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
onClick: (item?: any) => void | Promise<void>;
|
||||
|
||||
// Confirmation dialog for destructive actions
|
||||
confirm?: {
|
||||
title: string;
|
||||
message: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
};
|
||||
|
||||
// Show/hide based on item properties
|
||||
show?: (item: any) => boolean;
|
||||
}
|
||||
|
||||
export interface ActionMenuProps {
|
||||
item?: any; // The data item this menu is for
|
||||
actions: ActionMenuItem[];
|
||||
|
||||
// Menu trigger customization
|
||||
trigger?: 'dots' | 'button' | 'custom';
|
||||
triggerLabel?: string;
|
||||
triggerIcon?: React.ComponentType<TablerIconsProps>;
|
||||
triggerProps?: any;
|
||||
customTrigger?: React.ReactNode;
|
||||
|
||||
// Menu positioning
|
||||
position?: 'bottom-end' | 'bottom-start' | 'top-end' | 'top-start';
|
||||
|
||||
// Menu styling
|
||||
withArrow?: boolean;
|
||||
withinPortal?: boolean;
|
||||
|
||||
// Accessibility
|
||||
'aria-label'?: string;
|
||||
}
|
||||
|
||||
const ActionMenu: React.FC<ActionMenuProps> = ({
|
||||
item,
|
||||
actions,
|
||||
trigger = 'dots',
|
||||
triggerLabel = 'Actions',
|
||||
triggerIcon: TriggerIcon = IconDots,
|
||||
triggerProps = {},
|
||||
customTrigger,
|
||||
position = 'bottom-end',
|
||||
withArrow = false,
|
||||
withinPortal = true,
|
||||
'aria-label': ariaLabel,
|
||||
}) => {
|
||||
// Filter actions based on show/hidden conditions
|
||||
const visibleActions = actions.filter(action => {
|
||||
if (action.hidden) return false;
|
||||
if (action.show && !action.show(item)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Group actions by type (with dividers)
|
||||
const groupedActions = groupActionsByType(visibleActions);
|
||||
|
||||
// Don't render if no visible actions
|
||||
if (visibleActions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleActionClick = async (action: ActionMenuItem) => {
|
||||
try {
|
||||
// Show confirmation dialog for destructive actions
|
||||
if (action.confirm) {
|
||||
return new Promise<void>((resolve) => {
|
||||
modals.openConfirmModal({
|
||||
title: action.confirm!.title,
|
||||
children: (
|
||||
<Text size="sm">{action.confirm!.message}</Text>
|
||||
),
|
||||
labels: {
|
||||
confirm: action.confirm!.confirmLabel || 'Confirm',
|
||||
cancel: action.confirm!.cancelLabel || 'Cancel'
|
||||
},
|
||||
confirmProps: { color: action.color || 'red' },
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await action.onClick(item);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error(`Action ${action.key} failed:`, error);
|
||||
notifications.show({
|
||||
title: 'Action Failed',
|
||||
message: `Failed to ${action.label.toLowerCase()}`,
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
},
|
||||
onCancel: () => resolve(),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await action.onClick(item);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Action ${action.key} failed:`, error);
|
||||
notifications.show({
|
||||
title: 'Action Failed',
|
||||
message: `Failed to ${action.label.toLowerCase()}`,
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderTrigger = () => {
|
||||
if (customTrigger) {
|
||||
return customTrigger;
|
||||
}
|
||||
|
||||
if (trigger === 'button') {
|
||||
return (
|
||||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
leftSection={<TriggerIcon size={16} />}
|
||||
{...triggerProps}
|
||||
>
|
||||
{triggerLabel}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Default dots trigger
|
||||
return (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
aria-label={ariaLabel || `${triggerLabel} menu`}
|
||||
{...triggerProps}
|
||||
>
|
||||
<TriggerIcon size={16} />
|
||||
</ActionIcon>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMenuItem = (action: ActionMenuItem) => {
|
||||
const IconComponent = action.icon;
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
key={action.key}
|
||||
leftSection={IconComponent && <IconComponent size={14} />}
|
||||
color={action.color}
|
||||
disabled={action.disabled}
|
||||
onClick={() => handleActionClick(action)}
|
||||
>
|
||||
{action.label}
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu
|
||||
position={position}
|
||||
withArrow={withArrow}
|
||||
withinPortal={withinPortal}
|
||||
>
|
||||
<Menu.Target>
|
||||
{renderTrigger()}
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
{groupedActions.map((group, groupIndex) => (
|
||||
<React.Fragment key={groupIndex}>
|
||||
{group.map(renderMenuItem)}
|
||||
{groupIndex < groupedActions.length - 1 && <Menu.Divider />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper function to group actions by type for better UX
|
||||
const groupActionsByType = (actions: ActionMenuItem[]): ActionMenuItem[][] => {
|
||||
const primaryActions: ActionMenuItem[] = [];
|
||||
const secondaryActions: ActionMenuItem[] = [];
|
||||
const destructiveActions: ActionMenuItem[] = [];
|
||||
|
||||
actions.forEach(action => {
|
||||
if (action.color === 'red' || action.key.includes('delete') || action.key.includes('remove')) {
|
||||
destructiveActions.push(action);
|
||||
} else if (action.key.includes('edit') || action.key.includes('view') || action.key.includes('copy')) {
|
||||
primaryActions.push(action);
|
||||
} else {
|
||||
secondaryActions.push(action);
|
||||
}
|
||||
});
|
||||
|
||||
const groups: ActionMenuItem[][] = [];
|
||||
if (primaryActions.length > 0) groups.push(primaryActions);
|
||||
if (secondaryActions.length > 0) groups.push(secondaryActions);
|
||||
if (destructiveActions.length > 0) groups.push(destructiveActions);
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
export default ActionMenu;
|
||||
|
||||
// Pre-built action creators for common operations
|
||||
export const createViewAction = (onView: (item: any) => void): ActionMenuItem => ({
|
||||
key: 'view',
|
||||
label: 'View Details',
|
||||
icon: IconEye,
|
||||
onClick: onView,
|
||||
});
|
||||
|
||||
export const createEditAction = (onEdit: (item: any) => void): ActionMenuItem => ({
|
||||
key: 'edit',
|
||||
label: 'Edit',
|
||||
icon: IconEdit,
|
||||
color: 'blue',
|
||||
onClick: onEdit,
|
||||
});
|
||||
|
||||
export const createCopyAction = (onCopy: (item: any) => void): ActionMenuItem => ({
|
||||
key: 'copy',
|
||||
label: 'Duplicate',
|
||||
icon: IconCopy,
|
||||
onClick: onCopy,
|
||||
});
|
||||
|
||||
export const createDeleteAction = (
|
||||
onDelete: (item: any) => void | Promise<void>,
|
||||
itemName = 'item'
|
||||
): ActionMenuItem => ({
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
icon: IconTrash,
|
||||
color: 'red',
|
||||
onClick: onDelete,
|
||||
confirm: {
|
||||
title: 'Confirm Delete',
|
||||
message: `Are you sure you want to delete this ${itemName}? This action cannot be undone.`,
|
||||
confirmLabel: 'Delete',
|
||||
cancelLabel: 'Cancel',
|
||||
},
|
||||
});
|
||||
|
||||
export const createArchiveAction = (onArchive: (item: any) => void): ActionMenuItem => ({
|
||||
key: 'archive',
|
||||
label: 'Archive',
|
||||
icon: IconArchive,
|
||||
color: 'orange',
|
||||
onClick: onArchive,
|
||||
confirm: {
|
||||
title: 'Archive Item',
|
||||
message: 'Are you sure you want to archive this item?',
|
||||
},
|
||||
});
|
||||
|
||||
export const createRestoreAction = (onRestore: (item: any) => void): ActionMenuItem => ({
|
||||
key: 'restore',
|
||||
label: 'Restore',
|
||||
icon: IconRestore,
|
||||
color: 'green',
|
||||
onClick: onRestore,
|
||||
});
|
||||
|
||||
// Context-specific action sets
|
||||
export const getUserActions = (
|
||||
onEdit: (item: any) => void,
|
||||
onDelete: (item: any) => void,
|
||||
onViewDetails?: (item: any) => void
|
||||
): ActionMenuItem[] => [
|
||||
...(onViewDetails ? [createViewAction(onViewDetails)] : []),
|
||||
createEditAction(onEdit),
|
||||
createDeleteAction(onDelete, 'user'),
|
||||
];
|
||||
|
||||
export const getApplicationActions = (
|
||||
onEdit: (item: any) => void,
|
||||
onDelete: (item: any) => void,
|
||||
onConfigure?: (item: any) => void
|
||||
): ActionMenuItem[] => [
|
||||
createEditAction(onEdit),
|
||||
...(onConfigure ? [{
|
||||
key: 'configure',
|
||||
label: 'Configure',
|
||||
icon: IconSettings,
|
||||
onClick: onConfigure,
|
||||
}] : []),
|
||||
createDeleteAction(onDelete, 'application'),
|
||||
];
|
||||
|
||||
export const getFunctionActions = (
|
||||
onEdit: (item: any) => void,
|
||||
onDelete: (item: any) => void,
|
||||
onExecute?: (item: any) => void,
|
||||
onViewLogs?: (item: any) => void
|
||||
): ActionMenuItem[] => [
|
||||
...(onExecute ? [{
|
||||
key: 'execute',
|
||||
label: 'Execute',
|
||||
icon: IconPlayerPlay,
|
||||
color: 'green',
|
||||
onClick: onExecute,
|
||||
}] : []),
|
||||
...(onViewLogs ? [{
|
||||
key: 'logs',
|
||||
label: 'View Logs',
|
||||
icon: IconEye,
|
||||
onClick: onViewLogs,
|
||||
}] : []),
|
||||
createEditAction(onEdit),
|
||||
createDeleteAction(onDelete, 'function'),
|
||||
];
|
||||
|
||||
export const getTokenActions = (
|
||||
onRevoke: (item: any) => void,
|
||||
onCopy?: (item: any) => void,
|
||||
onRefresh?: (item: any) => void
|
||||
): ActionMenuItem[] => [
|
||||
...(onCopy ? [createCopyAction(onCopy)] : []),
|
||||
...(onRefresh ? [{
|
||||
key: 'refresh',
|
||||
label: 'Refresh',
|
||||
icon: IconRefresh,
|
||||
onClick: onRefresh,
|
||||
}] : []),
|
||||
{
|
||||
key: 'revoke',
|
||||
label: 'Revoke',
|
||||
icon: IconPlayerStop,
|
||||
color: 'red',
|
||||
onClick: onRevoke,
|
||||
confirm: {
|
||||
title: 'Revoke Token',
|
||||
message: 'Are you sure you want to revoke this token? This action cannot be undone and will immediately disable the token.',
|
||||
confirmLabel: 'Revoke',
|
||||
},
|
||||
},
|
||||
];
|
||||
346
web-components/src/components/EmptyState/EmptyState.tsx
Normal file
346
web-components/src/components/EmptyState/EmptyState.tsx
Normal file
@ -0,0 +1,346 @@
|
||||
import React from 'react';
|
||||
import { Stack, Text, Button, Center, Box } from '@mantine/core';
|
||||
import {
|
||||
IconDatabase,
|
||||
IconUsers,
|
||||
IconApps,
|
||||
IconFunction,
|
||||
IconKey,
|
||||
IconSearch,
|
||||
IconFilter,
|
||||
IconPlus,
|
||||
IconRefresh,
|
||||
IconAlertCircle,
|
||||
TablerIconsProps,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
export type EmptyStateVariant =
|
||||
| 'no-data'
|
||||
| 'no-results'
|
||||
| 'error'
|
||||
| 'loading-failed'
|
||||
| 'access-denied'
|
||||
| 'coming-soon';
|
||||
|
||||
export type EmptyStateContext =
|
||||
| 'users'
|
||||
| 'applications'
|
||||
| 'functions'
|
||||
| 'tokens'
|
||||
| 'executions'
|
||||
| 'permissions'
|
||||
| 'audit'
|
||||
| 'generic';
|
||||
|
||||
export interface EmptyStateAction {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
variant?: 'filled' | 'light' | 'outline';
|
||||
color?: string;
|
||||
leftSection?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface EmptyStateProps {
|
||||
variant?: EmptyStateVariant;
|
||||
context?: EmptyStateContext;
|
||||
title?: string;
|
||||
message?: string;
|
||||
icon?: React.ComponentType<TablerIconsProps>;
|
||||
iconSize?: number;
|
||||
iconColor?: string;
|
||||
actions?: EmptyStateAction[];
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
// Default icons for each context
|
||||
const CONTEXT_ICONS: Record<EmptyStateContext, React.ComponentType<TablerIconsProps>> = {
|
||||
users: IconUsers,
|
||||
applications: IconApps,
|
||||
functions: IconFunction,
|
||||
tokens: IconKey,
|
||||
executions: IconFunction,
|
||||
permissions: IconKey,
|
||||
audit: IconDatabase,
|
||||
generic: IconDatabase,
|
||||
};
|
||||
|
||||
// Default messages based on variant and context
|
||||
const getDefaultContent = (variant: EmptyStateVariant, context: EmptyStateContext) => {
|
||||
const contextNames: Record<EmptyStateContext, string> = {
|
||||
users: 'users',
|
||||
applications: 'applications',
|
||||
functions: 'functions',
|
||||
tokens: 'tokens',
|
||||
executions: 'executions',
|
||||
permissions: 'permissions',
|
||||
audit: 'audit events',
|
||||
generic: 'items',
|
||||
};
|
||||
|
||||
const contextName = contextNames[context];
|
||||
const capitalizedContext = contextName.charAt(0).toUpperCase() + contextName.slice(1);
|
||||
|
||||
switch (variant) {
|
||||
case 'no-data':
|
||||
return {
|
||||
title: `No ${contextName} found`,
|
||||
message: `You haven't created any ${contextName} yet. Get started by adding your first ${contextName.slice(0, -1)}.`,
|
||||
};
|
||||
|
||||
case 'no-results':
|
||||
return {
|
||||
title: 'No matching results',
|
||||
message: `No ${contextName} match your current filters or search criteria. Try adjusting your search terms or clearing filters.`,
|
||||
};
|
||||
|
||||
case 'error':
|
||||
return {
|
||||
title: 'Something went wrong',
|
||||
message: `We couldn't load your ${contextName}. Please try again or contact support if the problem persists.`,
|
||||
};
|
||||
|
||||
case 'loading-failed':
|
||||
return {
|
||||
title: 'Failed to load data',
|
||||
message: `There was a problem loading ${contextName}. Check your connection and try again.`,
|
||||
};
|
||||
|
||||
case 'access-denied':
|
||||
return {
|
||||
title: 'Access denied',
|
||||
message: `You don't have permission to view ${contextName}. Contact your administrator if you need access.`,
|
||||
};
|
||||
|
||||
case 'coming-soon':
|
||||
return {
|
||||
title: 'Coming soon',
|
||||
message: `${capitalizedContext} functionality is being developed. Check back soon for updates.`,
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
title: `No ${contextName}`,
|
||||
message: `There are no ${contextName} to display.`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Default actions based on variant and context
|
||||
const getDefaultActions = (
|
||||
variant: EmptyStateVariant,
|
||||
context: EmptyStateContext,
|
||||
onAdd?: () => void,
|
||||
onRefresh?: () => void,
|
||||
onClearFilters?: () => void
|
||||
): EmptyStateAction[] => {
|
||||
const actions: EmptyStateAction[] = [];
|
||||
|
||||
switch (variant) {
|
||||
case 'no-data':
|
||||
if (onAdd) {
|
||||
const contextActions: Record<EmptyStateContext, EmptyStateAction> = {
|
||||
users: {
|
||||
label: 'Add User',
|
||||
onClick: onAdd,
|
||||
variant: 'filled',
|
||||
leftSection: <IconPlus size={16} />,
|
||||
},
|
||||
applications: {
|
||||
label: 'Create Application',
|
||||
onClick: onAdd,
|
||||
variant: 'filled',
|
||||
leftSection: <IconPlus size={16} />,
|
||||
},
|
||||
functions: {
|
||||
label: 'Create Function',
|
||||
onClick: onAdd,
|
||||
variant: 'filled',
|
||||
leftSection: <IconPlus size={16} />,
|
||||
},
|
||||
tokens: {
|
||||
label: 'Generate Token',
|
||||
onClick: onAdd,
|
||||
variant: 'filled',
|
||||
leftSection: <IconPlus size={16} />,
|
||||
},
|
||||
executions: {
|
||||
label: 'Run Function',
|
||||
onClick: onAdd,
|
||||
variant: 'filled',
|
||||
leftSection: <IconFunction size={16} />,
|
||||
},
|
||||
permissions: {
|
||||
label: 'Add Permission',
|
||||
onClick: onAdd,
|
||||
variant: 'filled',
|
||||
leftSection: <IconPlus size={16} />,
|
||||
},
|
||||
audit: {
|
||||
label: 'Refresh',
|
||||
onClick: onAdd,
|
||||
variant: 'light',
|
||||
leftSection: <IconRefresh size={16} />,
|
||||
},
|
||||
generic: {
|
||||
label: 'Add New',
|
||||
onClick: onAdd,
|
||||
variant: 'filled',
|
||||
leftSection: <IconPlus size={16} />,
|
||||
},
|
||||
};
|
||||
actions.push(contextActions[context]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'no-results':
|
||||
if (onClearFilters) {
|
||||
actions.push({
|
||||
label: 'Clear Filters',
|
||||
onClick: onClearFilters,
|
||||
variant: 'light',
|
||||
leftSection: <IconFilter size={16} />,
|
||||
});
|
||||
}
|
||||
if (onRefresh) {
|
||||
actions.push({
|
||||
label: 'Refresh',
|
||||
onClick: onRefresh,
|
||||
variant: 'outline',
|
||||
leftSection: <IconRefresh size={16} />,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
case 'loading-failed':
|
||||
if (onRefresh) {
|
||||
actions.push({
|
||||
label: 'Try Again',
|
||||
onClick: onRefresh,
|
||||
variant: 'filled',
|
||||
leftSection: <IconRefresh size={16} />,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
// Default icon based on variant
|
||||
const getVariantIcon = (variant: EmptyStateVariant): React.ComponentType<TablerIconsProps> => {
|
||||
switch (variant) {
|
||||
case 'no-results':
|
||||
return IconSearch;
|
||||
case 'error':
|
||||
case 'loading-failed':
|
||||
case 'access-denied':
|
||||
return IconAlertCircle;
|
||||
default:
|
||||
return IconDatabase;
|
||||
}
|
||||
};
|
||||
|
||||
const EmptyState: React.FC<EmptyStateProps & {
|
||||
onAdd?: () => void;
|
||||
onRefresh?: () => void;
|
||||
onClearFilters?: () => void;
|
||||
}> = ({
|
||||
variant = 'no-data',
|
||||
context = 'generic',
|
||||
title,
|
||||
message,
|
||||
icon,
|
||||
iconSize = 48,
|
||||
iconColor = 'dimmed',
|
||||
actions,
|
||||
height = 400,
|
||||
onAdd,
|
||||
onRefresh,
|
||||
onClearFilters,
|
||||
}) => {
|
||||
const defaultContent = getDefaultContent(variant, context);
|
||||
const finalTitle = title || defaultContent.title;
|
||||
const finalMessage = message || defaultContent.message;
|
||||
|
||||
const IconComponent = icon || CONTEXT_ICONS[context] || getVariantIcon(variant);
|
||||
|
||||
const finalActions = actions || getDefaultActions(
|
||||
variant,
|
||||
context,
|
||||
onAdd,
|
||||
onRefresh,
|
||||
onClearFilters
|
||||
);
|
||||
|
||||
return (
|
||||
<Center h={height}>
|
||||
<Stack align="center" gap="lg" maw={400} ta="center">
|
||||
<Box c={iconColor}>
|
||||
<IconComponent size={iconSize} stroke={1.5} />
|
||||
</Box>
|
||||
|
||||
<Stack align="center" gap="xs">
|
||||
<Text size="lg" fw={600} c="dimmed">
|
||||
{finalTitle}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" lh={1.5}>
|
||||
{finalMessage}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{finalActions.length > 0 && (
|
||||
<Stack align="center" gap="sm" w="100%">
|
||||
{finalActions.map((action, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
onClick={action.onClick}
|
||||
variant={action.variant || 'filled'}
|
||||
color={action.color}
|
||||
leftSection={action.leftSection}
|
||||
size="sm"
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyState;
|
||||
|
||||
// Convenience components for common scenarios
|
||||
export const NoUsersState: React.FC<Omit<EmptyStateProps, 'context' | 'variant'> & { onAddUser?: () => void }> =
|
||||
({ onAddUser, ...props }) => (
|
||||
<EmptyState {...props} variant="no-data" context="users" onAdd={onAddUser} />
|
||||
);
|
||||
|
||||
export const NoApplicationsState: React.FC<Omit<EmptyStateProps, 'context' | 'variant'> & { onCreateApp?: () => void }> =
|
||||
({ onCreateApp, ...props }) => (
|
||||
<EmptyState {...props} variant="no-data" context="applications" onAdd={onCreateApp} />
|
||||
);
|
||||
|
||||
export const NoFunctionsState: React.FC<Omit<EmptyStateProps, 'context' | 'variant'> & { onCreateFunction?: () => void }> =
|
||||
({ onCreateFunction, ...props }) => (
|
||||
<EmptyState {...props} variant="no-data" context="functions" onAdd={onCreateFunction} />
|
||||
);
|
||||
|
||||
export const NoTokensState: React.FC<Omit<EmptyStateProps, 'context' | 'variant'> & { onGenerateToken?: () => void }> =
|
||||
({ onGenerateToken, ...props }) => (
|
||||
<EmptyState {...props} variant="no-data" context="tokens" onAdd={onGenerateToken} />
|
||||
);
|
||||
|
||||
export const NoSearchResults: React.FC<Omit<EmptyStateProps, 'variant'> & {
|
||||
onClearFilters?: () => void;
|
||||
onRefresh?: () => void;
|
||||
}> = ({ onClearFilters, onRefresh, ...props }) => (
|
||||
<EmptyState {...props} variant="no-results" onClearFilters={onClearFilters} onRefresh={onRefresh} />
|
||||
);
|
||||
|
||||
export const ErrorState: React.FC<Omit<EmptyStateProps, 'variant'> & { onRetry?: () => void }> =
|
||||
({ onRetry, ...props }) => (
|
||||
<EmptyState {...props} variant="error" onRefresh={onRetry} />
|
||||
);
|
||||
378
web-components/src/components/LoadingState/LoadingState.tsx
Normal file
378
web-components/src/components/LoadingState/LoadingState.tsx
Normal file
@ -0,0 +1,378 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Stack,
|
||||
Text,
|
||||
Loader,
|
||||
Center,
|
||||
Progress,
|
||||
Box,
|
||||
Group,
|
||||
Skeleton,
|
||||
Card,
|
||||
SimpleGrid,
|
||||
} from '@mantine/core';
|
||||
|
||||
export type LoadingVariant =
|
||||
| 'spinner' // Simple spinner with text
|
||||
| 'progress' // Progress bar with percentage
|
||||
| 'skeleton-table' // Table skeleton
|
||||
| 'skeleton-cards' // Card grid skeleton
|
||||
| 'skeleton-form' // Form skeleton
|
||||
| 'skeleton-text' // Text content skeleton
|
||||
| 'dots' // Animated dots
|
||||
| 'overlay'; // Full overlay spinner
|
||||
|
||||
export type LoadingSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
export interface LoadingStateProps {
|
||||
variant?: LoadingVariant;
|
||||
size?: LoadingSize;
|
||||
height?: number | string;
|
||||
|
||||
// Text content
|
||||
message?: string;
|
||||
submessage?: string;
|
||||
|
||||
// Progress specific
|
||||
progress?: number; // 0-100
|
||||
progressLabel?: string;
|
||||
|
||||
// Skeleton specific
|
||||
rows?: number;
|
||||
columns?: number;
|
||||
|
||||
// Styling
|
||||
color?: string;
|
||||
withContainer?: boolean;
|
||||
|
||||
// Animation
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
const LoadingState: React.FC<LoadingStateProps> = ({
|
||||
variant = 'spinner',
|
||||
size = 'md',
|
||||
height = 200,
|
||||
message,
|
||||
submessage,
|
||||
progress,
|
||||
progressLabel,
|
||||
rows = 5,
|
||||
columns = 3,
|
||||
color = 'blue',
|
||||
withContainer = true,
|
||||
animate = true,
|
||||
}) => {
|
||||
const getLoaderSize = (): number => {
|
||||
const sizeMap: Record<LoadingSize, number> = {
|
||||
xs: 16,
|
||||
sm: 24,
|
||||
md: 32,
|
||||
lg: 48,
|
||||
xl: 64,
|
||||
};
|
||||
return sizeMap[size];
|
||||
};
|
||||
|
||||
const getTextSize = (): string => {
|
||||
const sizeMap: Record<LoadingSize, string> = {
|
||||
xs: 'xs',
|
||||
sm: 'sm',
|
||||
md: 'md',
|
||||
lg: 'lg',
|
||||
xl: 'xl',
|
||||
};
|
||||
return sizeMap[size];
|
||||
};
|
||||
|
||||
const renderSpinner = () => (
|
||||
<Stack align="center" gap="md">
|
||||
<Loader size={getLoaderSize()} color={color} />
|
||||
{message && (
|
||||
<Text size={getTextSize()} c="dimmed" ta="center">
|
||||
{message}
|
||||
</Text>
|
||||
)}
|
||||
{submessage && (
|
||||
<Text size="xs" c="dimmed" ta="center">
|
||||
{submessage}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const renderProgress = () => (
|
||||
<Stack gap="md">
|
||||
{(message || progressLabel) && (
|
||||
<Group justify="space-between">
|
||||
<Text size={getTextSize()}>
|
||||
{message || 'Loading...'}
|
||||
</Text>
|
||||
{progressLabel && (
|
||||
<Text size="sm" c="dimmed">
|
||||
{progressLabel}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Progress
|
||||
value={progress || 0}
|
||||
color={color}
|
||||
size={size}
|
||||
animated={animate}
|
||||
/>
|
||||
|
||||
{submessage && (
|
||||
<Text size="xs" c="dimmed" ta="center">
|
||||
{submessage}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const renderSkeletonTable = () => (
|
||||
<Stack gap="xs">
|
||||
{/* Table header */}
|
||||
<Group gap="md">
|
||||
{Array.from({ length: columns }).map((_, i) => (
|
||||
<Skeleton key={`header-${i}`} height={20} width={`${100 / columns}%`} />
|
||||
))}
|
||||
</Group>
|
||||
|
||||
{/* Table rows */}
|
||||
{Array.from({ length: rows }).map((_, rowIndex) => (
|
||||
<Group key={`row-${rowIndex}`} gap="md">
|
||||
{Array.from({ length: columns }).map((_, colIndex) => (
|
||||
<Skeleton
|
||||
key={`cell-${rowIndex}-${colIndex}`}
|
||||
height={16}
|
||||
width={`${100 / columns}%`}
|
||||
/>
|
||||
))}
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const renderSkeletonCards = () => (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: columns }}>
|
||||
{Array.from({ length: rows * columns }).map((_, i) => (
|
||||
<Card key={`card-${i}`} padding="md" withBorder>
|
||||
<Stack gap="xs">
|
||||
<Skeleton height={20} width="70%" />
|
||||
<Skeleton height={14} />
|
||||
<Skeleton height={14} width="90%" />
|
||||
<Group justify="apart" mt="md">
|
||||
<Skeleton height={12} width="40%" />
|
||||
<Skeleton height={12} width="30%" />
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
);
|
||||
|
||||
const renderSkeletonForm = () => (
|
||||
<Stack gap="md">
|
||||
{Array.from({ length: rows }).map((_, i) => (
|
||||
<Box key={`form-field-${i}`}>
|
||||
<Skeleton height={12} width="30%" mb="xs" />
|
||||
<Skeleton height={36} />
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Group justify="flex-end" mt="xl">
|
||||
<Skeleton height={36} width={80} />
|
||||
<Skeleton height={36} width={100} />
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const renderSkeletonText = () => (
|
||||
<Stack gap="xs">
|
||||
{Array.from({ length: rows }).map((_, i) => {
|
||||
// Vary line widths for more realistic skeleton
|
||||
const widths = ['100%', '95%', '85%', '90%', '75%'];
|
||||
return (
|
||||
<Skeleton
|
||||
key={`text-${i}`}
|
||||
height={16}
|
||||
width={widths[i % widths.length]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const renderDots = () => {
|
||||
const dots = Array.from({ length: 3 }).map((_, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
w={8}
|
||||
h={8}
|
||||
bg={color}
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
animation: animate ? `loading-dots 1.4s infinite ease-in-out ${i * 0.16}s` : undefined,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<Stack align="center" gap="md">
|
||||
<Group gap="xs">
|
||||
{dots}
|
||||
</Group>
|
||||
{message && (
|
||||
<Text size={getTextSize()} c="dimmed" ta="center">
|
||||
{message}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOverlay = () => (
|
||||
<Box
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
backdropFilter: 'blur(2px)',
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<Center h="100%">
|
||||
{renderSpinner()}
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const getContent = () => {
|
||||
switch (variant) {
|
||||
case 'progress':
|
||||
return renderProgress();
|
||||
case 'skeleton-table':
|
||||
return renderSkeletonTable();
|
||||
case 'skeleton-cards':
|
||||
return renderSkeletonCards();
|
||||
case 'skeleton-form':
|
||||
return renderSkeletonForm();
|
||||
case 'skeleton-text':
|
||||
return renderSkeletonText();
|
||||
case 'dots':
|
||||
return renderDots();
|
||||
case 'overlay':
|
||||
return renderOverlay();
|
||||
default:
|
||||
return renderSpinner();
|
||||
}
|
||||
};
|
||||
|
||||
// For overlay variant, don't wrap in container
|
||||
if (variant === 'overlay') {
|
||||
return (
|
||||
<>
|
||||
{getContent()}
|
||||
<style>{loadingDotsKeyframes}</style>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{getContent()}
|
||||
{animate && <style>{loadingDotsKeyframes}</style>}
|
||||
</>
|
||||
);
|
||||
|
||||
if (!withContainer) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<Center h={height}>
|
||||
{content}
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
// CSS keyframes for dot animation
|
||||
const loadingDotsKeyframes = `
|
||||
@keyframes loading-dots {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default LoadingState;
|
||||
|
||||
// Convenience components for common loading scenarios
|
||||
export const TableLoadingState: React.FC<{ rows?: number; columns?: number }> =
|
||||
({ rows = 5, columns = 4 }) => (
|
||||
<LoadingState variant="skeleton-table" rows={rows} columns={columns} withContainer={false} />
|
||||
);
|
||||
|
||||
export const CardsLoadingState: React.FC<{ count?: number; columns?: number }> =
|
||||
({ count = 6, columns = 3 }) => (
|
||||
<LoadingState
|
||||
variant="skeleton-cards"
|
||||
rows={Math.ceil(count / columns)}
|
||||
columns={columns}
|
||||
withContainer={false}
|
||||
/>
|
||||
);
|
||||
|
||||
export const FormLoadingState: React.FC<{ fields?: number }> =
|
||||
({ fields = 4 }) => (
|
||||
<LoadingState variant="skeleton-form" rows={fields} withContainer={false} />
|
||||
);
|
||||
|
||||
export const PageLoadingState: React.FC<{ message?: string }> =
|
||||
({ message = 'Loading page...' }) => (
|
||||
<LoadingState variant="spinner" message={message} height="60vh" size="lg" />
|
||||
);
|
||||
|
||||
export const InlineLoadingState: React.FC<{ message?: string; size?: LoadingSize }> =
|
||||
({ message = 'Loading...', size = 'sm' }) => (
|
||||
<Group gap="xs">
|
||||
<Loader size={size === 'xs' ? 12 : size === 'sm' ? 16 : 20} />
|
||||
<Text size={size} c="dimmed">{message}</Text>
|
||||
</Group>
|
||||
);
|
||||
|
||||
// Hook for loading state management
|
||||
export const useLoadingState = (initialLoading = false) => {
|
||||
const [loading, setLoading] = React.useState(initialLoading);
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
|
||||
const startLoading = React.useCallback(() => setLoading(true), []);
|
||||
const stopLoading = React.useCallback(() => {
|
||||
setLoading(false);
|
||||
setProgress(0);
|
||||
}, []);
|
||||
|
||||
const updateProgress = React.useCallback((value: number) => {
|
||||
setProgress(Math.max(0, Math.min(100, value)));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
loading,
|
||||
progress,
|
||||
startLoading,
|
||||
stopLoading,
|
||||
updateProgress,
|
||||
setLoading,
|
||||
setProgress,
|
||||
};
|
||||
};
|
||||
266
web-components/src/components/Sidebar/Sidebar.tsx
Normal file
266
web-components/src/components/Sidebar/Sidebar.tsx
Normal file
@ -0,0 +1,266 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Group,
|
||||
Title,
|
||||
ActionIcon,
|
||||
ScrollArea,
|
||||
Box,
|
||||
Divider,
|
||||
} from '@mantine/core';
|
||||
import { IconX } from '@tabler/icons-react';
|
||||
|
||||
export interface SidebarProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
width?: number;
|
||||
position?: 'left' | 'right';
|
||||
headerActions?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
|
||||
// Styling customization
|
||||
zIndex?: number;
|
||||
offsetTop?: number;
|
||||
backgroundColor?: string;
|
||||
borderColor?: string;
|
||||
|
||||
// Animation
|
||||
animationDuration?: string;
|
||||
|
||||
// Accessibility
|
||||
'aria-label'?: string;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({
|
||||
opened,
|
||||
onClose,
|
||||
title,
|
||||
width = 450,
|
||||
position = 'right',
|
||||
headerActions,
|
||||
footer,
|
||||
children,
|
||||
zIndex = 1000,
|
||||
offsetTop = 60,
|
||||
backgroundColor = 'var(--mantine-color-body)',
|
||||
borderColor = 'var(--mantine-color-gray-3)',
|
||||
animationDuration = '0.3s',
|
||||
'aria-label': ariaLabel,
|
||||
}) => {
|
||||
// Calculate position styles based on position prop
|
||||
const getPositionStyles = () => {
|
||||
const baseStyles = {
|
||||
position: 'fixed' as const,
|
||||
top: offsetTop,
|
||||
bottom: 0,
|
||||
width: `${width}px`,
|
||||
zIndex,
|
||||
borderRadius: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
backgroundColor,
|
||||
transition: `${position} ${animationDuration} ease`,
|
||||
};
|
||||
|
||||
if (position === 'right') {
|
||||
return {
|
||||
...baseStyles,
|
||||
right: opened ? 0 : `-${width}px`,
|
||||
borderLeft: `1px solid ${borderColor}`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...baseStyles,
|
||||
left: opened ? 0 : `-${width}px`,
|
||||
borderRight: `1px solid ${borderColor}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
style={getPositionStyles()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={ariaLabel || title}
|
||||
aria-hidden={!opened}
|
||||
>
|
||||
{/* Header */}
|
||||
<Group
|
||||
justify="space-between"
|
||||
p="md"
|
||||
style={{ borderBottom: `1px solid ${borderColor}` }}
|
||||
>
|
||||
<Title order={4} style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{title}
|
||||
</Title>
|
||||
|
||||
{/* Header actions (optional) */}
|
||||
{headerActions && (
|
||||
<Group gap="xs">
|
||||
{headerActions}
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{/* Close button */}
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={onClose}
|
||||
aria-label="Close sidebar"
|
||||
ml="xs"
|
||||
>
|
||||
<IconX size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
|
||||
{/* Content */}
|
||||
<ScrollArea style={{ flex: 1 }} scrollbarSize={6}>
|
||||
<Box p="md">
|
||||
{children}
|
||||
</Box>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Footer (optional) */}
|
||||
{footer && (
|
||||
<>
|
||||
<Divider />
|
||||
<Box p="md" style={{ borderTop: `1px solid ${borderColor}` }}>
|
||||
{footer}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
||||
// Higher-level convenience component that combines Sidebar with common form patterns
|
||||
export interface FormSidebarWrapperProps extends Omit<SidebarProps, 'children'> {
|
||||
children: React.ReactNode;
|
||||
cancelLabel?: string;
|
||||
submitLabel?: string;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: () => void;
|
||||
submitDisabled?: boolean;
|
||||
showFooterActions?: boolean;
|
||||
}
|
||||
|
||||
export const FormSidebarWrapper: React.FC<FormSidebarWrapperProps> = ({
|
||||
children,
|
||||
cancelLabel = 'Cancel',
|
||||
submitLabel = 'Save',
|
||||
onCancel,
|
||||
onSubmit,
|
||||
submitDisabled = false,
|
||||
showFooterActions = true,
|
||||
onClose,
|
||||
...sidebarProps
|
||||
}) => {
|
||||
const handleCancel = () => {
|
||||
onCancel?.();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const footer = showFooterActions ? (
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
onClick={handleCancel}
|
||||
size="sm"
|
||||
>
|
||||
{cancelLabel}
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
onClick={onSubmit}
|
||||
disabled={submitDisabled}
|
||||
size="sm"
|
||||
>
|
||||
{submitLabel}
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
{...sidebarProps}
|
||||
onClose={onClose}
|
||||
footer={footer}
|
||||
>
|
||||
{children}
|
||||
</Sidebar>
|
||||
);
|
||||
};
|
||||
|
||||
// Specialized sidebar variants for common use cases
|
||||
export interface DetailsSidebarProps extends Omit<SidebarProps, 'title'> {
|
||||
itemName: string;
|
||||
itemType?: string;
|
||||
editButton?: React.ReactNode;
|
||||
deleteButton?: React.ReactNode;
|
||||
status?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const DetailsSidebar: React.FC<DetailsSidebarProps> = ({
|
||||
itemName,
|
||||
itemType = 'Item',
|
||||
editButton,
|
||||
deleteButton,
|
||||
status,
|
||||
children,
|
||||
...sidebarProps
|
||||
}) => {
|
||||
const headerActions = (
|
||||
<Group gap="xs">
|
||||
{status}
|
||||
{editButton}
|
||||
{deleteButton}
|
||||
</Group>
|
||||
);
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
{...sidebarProps}
|
||||
title={`${itemType}: ${itemName}`}
|
||||
headerActions={headerActions}
|
||||
>
|
||||
{children}
|
||||
</Sidebar>
|
||||
);
|
||||
};
|
||||
|
||||
// Quick sidebar for simple content display
|
||||
export interface QuickSidebarProps extends Omit<SidebarProps, 'children'> {
|
||||
content: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const QuickSidebar: React.FC<QuickSidebarProps> = ({
|
||||
content,
|
||||
actions,
|
||||
...sidebarProps
|
||||
}) => (
|
||||
<Sidebar {...sidebarProps} footer={actions}>
|
||||
{content}
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
// Hooks for sidebar state management
|
||||
export const useSidebar = (initialOpened = false) => {
|
||||
const [opened, setOpened] = React.useState(initialOpened);
|
||||
|
||||
const open = React.useCallback(() => setOpened(true), []);
|
||||
const close = React.useCallback(() => setOpened(false), []);
|
||||
const toggle = React.useCallback(() => setOpened(prev => !prev), []);
|
||||
|
||||
return {
|
||||
opened,
|
||||
open,
|
||||
close,
|
||||
toggle,
|
||||
setOpened,
|
||||
};
|
||||
};
|
||||
186
web-components/src/components/StatusBadge/StatusBadge.tsx
Normal file
186
web-components/src/components/StatusBadge/StatusBadge.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import React from 'react';
|
||||
import { Badge, BadgeProps } from '@mantine/core';
|
||||
|
||||
export type StatusVariant = 'status' | 'role' | 'runtime' | 'type' | 'severity' | 'execution';
|
||||
|
||||
export interface StatusBadgeProps extends Omit<BadgeProps, 'color' | 'children'> {
|
||||
value: string;
|
||||
variant?: StatusVariant;
|
||||
customColorMap?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Standardized color mappings across all Skybridge apps
|
||||
const COLOR_MAPS: Record<StatusVariant, Record<string, string>> = {
|
||||
// Common status colors (used in User, KMS, etc.)
|
||||
status: {
|
||||
active: 'green',
|
||||
inactive: 'gray',
|
||||
pending: 'yellow',
|
||||
suspended: 'red',
|
||||
enabled: 'green',
|
||||
disabled: 'gray',
|
||||
online: 'green',
|
||||
offline: 'gray',
|
||||
running: 'green',
|
||||
stopped: 'gray',
|
||||
paused: 'yellow',
|
||||
failed: 'red',
|
||||
success: 'green',
|
||||
completed: 'green',
|
||||
error: 'red',
|
||||
warning: 'yellow',
|
||||
info: 'blue',
|
||||
},
|
||||
|
||||
// User roles (User Management)
|
||||
role: {
|
||||
admin: 'red',
|
||||
moderator: 'orange',
|
||||
user: 'blue',
|
||||
viewer: 'gray',
|
||||
owner: 'purple',
|
||||
editor: 'cyan',
|
||||
contributor: 'teal',
|
||||
guest: 'gray',
|
||||
},
|
||||
|
||||
// Application types (KMS)
|
||||
type: {
|
||||
static: 'blue',
|
||||
user: 'cyan',
|
||||
service: 'green',
|
||||
application: 'purple',
|
||||
api: 'orange',
|
||||
web: 'teal',
|
||||
mobile: 'pink',
|
||||
desktop: 'indigo',
|
||||
},
|
||||
|
||||
// Runtime environments (FaaS)
|
||||
runtime: {
|
||||
'nodejs18': 'green',
|
||||
'nodejs20': 'lime',
|
||||
'python3.9': 'blue',
|
||||
'python3.11': 'indigo',
|
||||
'go1.20': 'cyan',
|
||||
'go1.21': 'teal',
|
||||
'java11': 'orange',
|
||||
'java17': 'red',
|
||||
'dotnet6': 'purple',
|
||||
'dotnet7': 'violet',
|
||||
'rust': 'dark',
|
||||
'php8': 'grape',
|
||||
},
|
||||
|
||||
// Audit severity levels (KMS Audit)
|
||||
severity: {
|
||||
critical: 'red',
|
||||
high: 'orange',
|
||||
medium: 'yellow',
|
||||
low: 'blue',
|
||||
info: 'gray',
|
||||
debug: 'dark',
|
||||
},
|
||||
|
||||
// Function execution states (FaaS)
|
||||
execution: {
|
||||
queued: 'gray',
|
||||
running: 'blue',
|
||||
succeeded: 'green',
|
||||
failed: 'red',
|
||||
timeout: 'orange',
|
||||
cancelled: 'yellow',
|
||||
retrying: 'cyan',
|
||||
},
|
||||
};
|
||||
|
||||
// Default fallback colors for unknown values
|
||||
const DEFAULT_COLORS: Record<StatusVariant, string> = {
|
||||
status: 'gray',
|
||||
role: 'blue',
|
||||
runtime: 'blue',
|
||||
type: 'blue',
|
||||
severity: 'gray',
|
||||
execution: 'gray',
|
||||
};
|
||||
|
||||
const StatusBadge: React.FC<StatusBadgeProps> = ({
|
||||
value,
|
||||
variant = 'status',
|
||||
customColorMap,
|
||||
size = 'sm',
|
||||
...badgeProps
|
||||
}) => {
|
||||
if (!value) {
|
||||
return <Badge color="gray" size={size} {...badgeProps}>-</Badge>;
|
||||
}
|
||||
|
||||
// Determine color from appropriate mapping
|
||||
const colorMap = customColorMap || COLOR_MAPS[variant] || COLOR_MAPS.status;
|
||||
const color = colorMap[value.toLowerCase()] || DEFAULT_COLORS[variant];
|
||||
|
||||
// Format display value (capitalize first letter, handle special cases)
|
||||
const displayValue = formatDisplayValue(value, variant);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
color={color}
|
||||
size={size}
|
||||
variant="filled"
|
||||
{...badgeProps}
|
||||
>
|
||||
{displayValue}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper function to format display values consistently
|
||||
const formatDisplayValue = (value: string, variant: StatusVariant): string => {
|
||||
// Special formatting for specific variants
|
||||
switch (variant) {
|
||||
case 'runtime':
|
||||
// Format runtime names nicely (e.g., "nodejs18" -> "Node.js 18")
|
||||
if (value.startsWith('nodejs')) return `Node.js ${value.replace('nodejs', '')}`;
|
||||
if (value.startsWith('python')) return `Python ${value.replace('python', '')}`;
|
||||
if (value.startsWith('go')) return `Go ${value.replace('go', '')}`;
|
||||
if (value.startsWith('java')) return `Java ${value.replace('java', '')}`;
|
||||
if (value.startsWith('dotnet')) return `.NET ${value.replace('dotnet', '')}`;
|
||||
break;
|
||||
|
||||
case 'type':
|
||||
// Capitalize application types
|
||||
if (value === 'api') return 'API';
|
||||
if (value === 'web') return 'Web App';
|
||||
if (value === 'mobile') return 'Mobile App';
|
||||
break;
|
||||
}
|
||||
|
||||
// Default: capitalize first letter
|
||||
return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
|
||||
};
|
||||
|
||||
export default StatusBadge;
|
||||
|
||||
// Export additional utilities for advanced use cases
|
||||
export { COLOR_MAPS, DEFAULT_COLORS };
|
||||
|
||||
// Convenience components for common use cases
|
||||
export const UserRoleBadge: React.FC<Omit<StatusBadgeProps, 'variant'>> = (props) => (
|
||||
<StatusBadge {...props} variant="role" />
|
||||
);
|
||||
|
||||
export const ApplicationTypeBadge: React.FC<Omit<StatusBadgeProps, 'variant'>> = (props) => (
|
||||
<StatusBadge {...props} variant="type" />
|
||||
);
|
||||
|
||||
export const RuntimeBadge: React.FC<Omit<StatusBadgeProps, 'variant'>> = (props) => (
|
||||
<StatusBadge {...props} variant="runtime" />
|
||||
);
|
||||
|
||||
export const ExecutionStatusBadge: React.FC<Omit<StatusBadgeProps, 'variant'>> = (props) => (
|
||||
<StatusBadge {...props} variant="execution" />
|
||||
);
|
||||
|
||||
export const SeverityBadge: React.FC<Omit<StatusBadgeProps, 'variant'>> = (props) => (
|
||||
<StatusBadge {...props} variant="severity" />
|
||||
);
|
||||
@ -1,6 +1,11 @@
|
||||
// Components
|
||||
export { default as FormSidebar } from './components/FormSidebar/FormSidebar';
|
||||
export { default as DataTable } from './components/DataTable/DataTable';
|
||||
export { default as StatusBadge, UserRoleBadge, ApplicationTypeBadge, RuntimeBadge, ExecutionStatusBadge, SeverityBadge } from './components/StatusBadge/StatusBadge';
|
||||
export { default as EmptyState, NoUsersState, NoApplicationsState, NoFunctionsState, NoTokensState, NoSearchResults, ErrorState } from './components/EmptyState/EmptyState';
|
||||
export { default as Sidebar, FormSidebarWrapper, DetailsSidebar, QuickSidebar, useSidebar } from './components/Sidebar/Sidebar';
|
||||
export { default as ActionMenu, createViewAction, createEditAction, createCopyAction, createDeleteAction, createArchiveAction, createRestoreAction, getUserActions, getApplicationActions, getFunctionActions, getTokenActions } from './components/ActionMenu/ActionMenu';
|
||||
export { default as LoadingState, TableLoadingState, CardsLoadingState, FormLoadingState, PageLoadingState, InlineLoadingState, useLoadingState } from './components/LoadingState/LoadingState';
|
||||
|
||||
// Types
|
||||
export * from './types';
|
||||
|
||||
@ -84,4 +84,11 @@ export interface FormField {
|
||||
options?: Array<{ value: string; label: string }>;
|
||||
validation?: ValidationRule;
|
||||
defaultValue?: any;
|
||||
}
|
||||
}
|
||||
|
||||
// Component-specific types
|
||||
export type { StatusVariant, StatusBadgeProps } from '../components/StatusBadge/StatusBadge';
|
||||
export type { EmptyStateVariant, EmptyStateContext, EmptyStateProps, EmptyStateAction } from '../components/EmptyState/EmptyState';
|
||||
export type { SidebarProps, FormSidebarWrapperProps, DetailsSidebarProps, QuickSidebarProps } from '../components/Sidebar/Sidebar';
|
||||
export type { ActionMenuItem, ActionMenuProps } from '../components/ActionMenu/ActionMenu';
|
||||
export type { LoadingVariant, LoadingSize, LoadingStateProps } from '../components/LoadingState/LoadingState';
|
||||
Reference in New Issue
Block a user