This commit is contained in:
2025-08-31 23:39:44 -04:00
parent 40f8780dec
commit d4f4747fde
20 changed files with 1919 additions and 13 deletions

View File

@ -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",

View File

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

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

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

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

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

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

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

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

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

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

View 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" />
);

View File

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

View File

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