Skip to content

feat(hooks): organize custom hooks into dedicated directory structure #1098

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/custom/ResourceDetailFormatters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
TableDataFormatter,
TextWithLinkFormatter
} from './Formatter';
import { useResourceCleanData } from './useResourceCleanData';
// Note: useResourceCleanData has been moved to src/hooks/data/
import { convertToReadableUnit, extractPodVolumnTables, splitCamelCaseString } from './utils';

export {
Expand All @@ -35,6 +35,5 @@ export {
splitCamelCaseString,
StatusFormatter,
TableDataFormatter,
TextWithLinkFormatter,
useResourceCleanData
TextWithLinkFormatter
};
9 changes: 1 addition & 8 deletions src/custom/Workspaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import WorkspaceEnvironmentSelection from './WorkspaceEnvironmentSelection';
import WorkspaceRecentActivityModal from './WorkspaceRecentActivityModal';
import WorkspaceTeamsTable from './WorkspaceTeamsTable';
import WorkspaceViewsTable from './WorkspaceViewsTable';
import useDesignAssignment from './hooks/useDesignAssignment';
import useEnvironmentAssignment from './hooks/useEnvironmentAssignment';
import useTeamAssignment from './hooks/useTeamAssignment';
import useViewAssignment from './hooks/useViewsAssignment';
// Note: Workspace hooks have been moved to src/hooks/workspace/
import { L5DeleteIcon, L5EditIcon } from './styles';

export {
Expand All @@ -19,10 +16,6 @@ export {
EnvironmentTable,
L5DeleteIcon,
L5EditIcon,
useDesignAssignment,
useEnvironmentAssignment,
useTeamAssignment,
useViewAssignment,
WorkspaceCard,
WorkspaceContentMoveModal,
WorkspaceEnvironmentSelection,
Expand Down
5 changes: 1 addition & 4 deletions src/custom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ import {
import { FeedbackButton } from './Feedback';
import { FlipCard, FlipCardProps } from './FlipCard';
import { FormatId } from './FormatId';
import { useWindowDimensions } from './Helpers/Dimension';
import { useNotificationHandler } from './Helpers/Notification';
// Note: useWindowDimensions and useNotificationHandler have been moved to src/hooks/
import { ColView, updateVisibleColumns } from './Helpers/ResponsiveColumns/responsive-coulmns.tsx';
import { LearningCard } from './LearningCard';
import { BasicMarkdown, RenderMarkdown } from './Markdown';
Expand Down Expand Up @@ -116,8 +115,6 @@ export {
UsersTable,
VisibilityChipMenu,
updateVisibleColumns,
useNotificationHandler,
useWindowDimensions,
withErrorBoundary,
withSuppressedErrorBoundary
};
Expand Down
118 changes: 118 additions & 0 deletions src/hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Sistent Hooks

This directory contains all reusable custom React hooks for the Sistent design system. The hooks are organized into logical categories for better maintainability and discoverability.

## Directory Structure

```
src/hooks/
├── data/ # Data management and API-related hooks
├── ui/ # UI interaction and state management hooks
├── utils/ # General utility hooks
├── workspace/ # Workspace-specific hooks
└── index.ts # Main exports
```

## Categories

### Data Hooks (`./data/`)

Hooks for managing data, API interactions, and data processing.

- `useRoomActivity` - WebSocket-based collaboration for room activity tracking
- `useResourceCleanData` - Resource data formatting and processing for Kubernetes resources

### UI Hooks (`./ui/`)

Hooks for managing UI state, interactions, and visual components.

- `useWindowDimensions` - Window dimension tracking with debounced resize handling
- `useNotification` - Notification management using notistack

### Utility Hooks (`./utils/`)

General-purpose utility hooks for common patterns.

- `useDebounce` - Debounce values with configurable delay
- `usePreventPageLeave` - Prevent users from leaving pages with unsaved changes
- `useLocalStorage` - Manage localStorage with React state synchronization
- `useToggle` - Simple boolean state toggle management
- `useTimeout` - Manage timeouts with automatic cleanup

### Workspace Hooks (`./workspace/`)

Hooks specific to workspace functionality and management.

- `useDesignAssignment` - Design assignment management for workspaces
- `useEnvironmentAssignment` - Environment assignment management for workspaces
- `useTeamAssignment` - Team assignment management for workspaces
- `useViewsAssignment` - Views assignment management for workspaces

## Usage

All hooks are exported from the main hooks index file and can be imported directly:

```typescript
import {
useDebounce,
useWindowDimensions,
useNotification,
useRoomActivity
} from '@layer5/sistent';
```

Or import specific categories:

```typescript
import { useDebounce, useToggle } from '@layer5/sistent';
```

## Adding New Hooks

When adding new hooks, please follow these guidelines:

1. **Categorization**: Place hooks in the appropriate directory based on their primary function
2. **Naming**: Use descriptive names starting with "use" following React conventions
3. **TypeScript**: Provide full type definitions for parameters and return values
4. **Documentation**: Include JSDoc comments describing the hook's purpose and usage
5. **Exports**: Add the hook to the appropriate category's index.ts file

## Examples

### Basic Usage

```typescript
import { useDebounce, useToggle } from '@layer5/sistent';

function MyComponent() {
const [searchTerm, setSearchTerm] = useState('');
const [isOpen, toggleOpen] = useToggle(false);
const debouncedSearchTerm = useDebounce(searchTerm, 300);

// Use debouncedSearchTerm for API calls
// Use toggleOpen for modal state
}
```

### Advanced Usage

```typescript
import { useLocalStorage, usePreventPageLeave } from '@layer5/sistent';

function FormComponent() {
const [formData, setFormData] = useLocalStorage('formData', {});
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

usePreventPageLeave(hasUnsavedChanges, 'You have unsaved changes!');

// Form logic here
}
```

## Best Practices

1. **Keep hooks focused**: Each hook should have a single, well-defined responsibility
2. **Use TypeScript**: Always provide type definitions for better developer experience
3. **Handle cleanup**: Ensure proper cleanup of effects, timers, and event listeners
4. **Test thoroughly**: Write comprehensive tests for all hooks
5. **Document edge cases**: Include documentation for error handling and edge cases
2 changes: 2 additions & 0 deletions src/hooks/data/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useResourceCleanData } from './useResourceCleanData';
export { useRoomActivity } from './useRoomActivity';
187 changes: 187 additions & 0 deletions src/hooks/data/useResourceCleanData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import _ from 'lodash';
import moment from 'moment';
import {
GetResourceCleanDataProps,
NumberState
} from '../../custom/ResourceDetailFormatters/types';

export const useResourceCleanData = () => {
const structureNumberStates = (parsedStatus: any, parsedSpec: any): NumberState[] => {
const numberStates: NumberState[] = [];

if (parsedSpec?.priority !== undefined) {
numberStates.push({
title: 'Priority',
value: parsedSpec.priority,
quantity: ''
});
}

if (parsedSpec?.containers) {
numberStates.push({
title: 'Containers',
value: parsedSpec.containers.length,
quantity: 'total'
});
}

if (parsedStatus?.containerStatuses) {
const totalRestarts = parsedStatus.containerStatuses.reduce(
(sum: number, container: { restartCount?: number }) => sum + (container.restartCount || 0),
0
);
numberStates.push({
title: 'Total Restarts',
value: totalRestarts,
quantity: 'times'
});
}

return numberStates;
};

const getAge = (creationTimestamp?: string): string | undefined => {
if (!creationTimestamp) return undefined;
const creationTime = moment(creationTimestamp);
const currentTime = moment();
const ageInHours = currentTime.diff(creationTime, 'hours');
return ageInHours >= 24 ? `${Math.floor(ageInHours / 24)} days` : `${ageInHours} hours`;
};

const getStatus = (attribute: any): string | false => {
if (attribute?.phase) {
return attribute.phase;
}
const readyCondition = attribute?.conditions?.find(
(cond: { type: string }) => cond.type === 'Ready'
);
return readyCondition ? 'Ready' : false;
};

const joinwithEqual = (object: Record<string, string> | undefined): string[] => {
if (!object) return [];
return Object.entries(object).map(([key, value]) => {
return `${key}=${value}`;
});
};

const getResourceCleanData = ({
resource,
activeLabels,
dispatchMsgToEditor,
router,
showStatus = true,
container
}: GetResourceCleanDataProps) => {
const parsedStatus = resource?.status?.attribute && JSON.parse(resource?.status?.attribute);
const parsedSpec = resource?.spec?.attribute && JSON.parse(resource?.spec.attribute);
const numberStates = structureNumberStates(parsedStatus, parsedSpec);
const kind = resource?.kind ?? resource?.component?.kind;
const cleanData = {
container: container,
age: getAge(resource?.metadata?.creationTimestamp),
kind: kind,
status: showStatus && getStatus(parsedStatus),
kubeletVersion: parsedStatus?.nodeInfo?.kubeletVersion,
podIP: parsedStatus?.podIP,
hostIP: parsedStatus?.hostIP,
QoSClass: parsedStatus?.qosClass,
size: parsedSpec?.resources?.requests?.storage,
claim: parsedSpec?.claimRef?.name,
claimNamespace: parsedSpec?.claimRef?.namespace,
apiVersion: resource?.apiVersion,
pods:
parsedStatus?.replicas === undefined
? parsedStatus?.availableReplicas?.toString()
: `${
parsedStatus?.availableReplicas?.toString() ?? '0'
} / ${parsedStatus?.replicas?.toString()}`,
replicas:
parsedStatus?.readyReplicas !== undefined &&
parsedStatus?.replicas !== undefined &&
`${parsedStatus?.readyReplicas} / ${parsedStatus?.replicas}`,
strategyType: resource?.configuration?.spec?.strategy?.type,
storageClass: parsedSpec?.storageClassName,
secretType: resource?.type,
serviceType: parsedSpec?.type,
clusterIp: parsedSpec?.clusterIP,
updateStrategy: parsedSpec?.updateStrategy?.type,
externalIp: parsedSpec?.externalIPs,
finalizers: parsedSpec?.finalizers,
accessModes: parsedSpec?.accessModes,
deeplinks: {
links: [
{ nodeName: parsedSpec?.nodeName, label: 'Node' },
{ namespace: resource?.metadata?.namespace, label: 'Namespace' },
{
serviceAccount: parsedSpec?.serviceAccountName,
label: 'ServiceAccount',
resourceCategory: 'Security'
}
],
router: router,
dispatchMsgToEditor: dispatchMsgToEditor
},
selector: parsedSpec?.selector?.matchLabels
? joinwithEqual(parsedSpec?.selector?.matchLabels)
: joinwithEqual(parsedSpec?.selector),
images: parsedSpec?.template?.spec?.containers?.map((container: { image?: string }) => {
return container?.image;
}),
numberStates: numberStates,
nodeSelector:
joinwithEqual(parsedSpec?.nodeSelector) ||
joinwithEqual(parsedSpec?.template?.spec?.nodeSelector),
loadBalancer: parsedStatus?.loadBalancer?.ingress?.map((ingress: { ip?: string }) => {
return ingress?.ip;
}),
rules: parsedSpec?.rules?.map((rule: { host?: string }) => {
return rule?.host;
}),
usage: {
allocatable: parsedStatus?.allocatable,
capacity: parsedStatus?.capacity
},
configData: resource?.configuration?.data,
capacity: parsedSpec?.capacity?.storage,
totalCapacity: parsedStatus?.capacity,
totalAllocatable: parsedStatus?.allocatable,
conditions: {
...parsedStatus?.conditions?.map((condition: { type?: string }) => {
return condition?.type;
})
},
tolerations: parsedSpec?.tolerations,
podVolumes: parsedSpec?.volumes,
ingressRules: parsedSpec?.rules,
connections: kind === 'Service' && _.omit(parsedSpec, ['selector', 'type']),
labels: {
data: resource?.metadata?.labels?.map((label) => {
const value = label?.value !== undefined ? label?.value : '';
return `${label?.key}=${value}`;
}),
dispatchMsgToEditor: dispatchMsgToEditor,
activeViewFilters: activeLabels
},
annotations: resource?.metadata?.annotations?.map((annotation) => {
const value = annotation?.value !== undefined ? annotation?.value : '';
return `${annotation?.key}=${value}`;
}),
// secret: resource?.data, //TODO: show it when we have the role based access control for secrets
initContainers: parsedSpec?.initContainers &&
parsedStatus?.initContainerStatuses && {
spec: parsedSpec?.initContainers,
status: parsedStatus?.initContainerStatuses
},
containers: parsedSpec?.containers &&
parsedStatus?.containerStatuses && {
spec: parsedSpec?.containers,
status: parsedStatus?.containerStatuses
}
};
return cleanData;
};

return { getResourceCleanData, structureNumberStates, getAge, getStatus, joinwithEqual };
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
MESHERY_CLOUD_STAGING,
MESHERY_CLOUD_WS_PROD,
MESHERY_CLOUD_WS_STAGING
} from '../constants/constants';
} from '../../constants/constants';

interface UserProfile {
[key: string]: unknown;
Expand Down
12 changes: 11 additions & 1 deletion src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
export * from './useRoomActivity';
// Data hooks
export * from './data';

// UI hooks
export * from './ui';

// Workspace hooks
export * from './workspace';

// Utility hooks
export * from './utils';
2 changes: 2 additions & 0 deletions src/hooks/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as useNotification } from './useNotification';
export { useWindowDimensions } from './useWindowSize';
Loading