Skip to content
Closed
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
231 changes: 231 additions & 0 deletions packages/contact-center/WidgetsProvider/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# Webex Contact Center Widgets Provider

This package provides a React provider component that automatically detects and tracks metrics for Contact Center widgets in your application.

## Features

- **Automatic Widget Detection**: The provider automatically scans for widgets in the DOM and tracks them without requiring manual HOC wrapping
- **Metrics Collection**: Collects performance and usage metrics for all detected widgets
- **Theme and Icon Support**: Integrates with Momentum Design System for consistent theming and iconography
- **Context-based Metrics**: Provides React context for accessing metrics data

## Installation

```bash
yarn add @webex/cc-widgets-provider
```

## Usage

### Basic Setup

Simply wrap your application with the `WidgetsProvider`:

```tsx
import React from 'react';
import WidgetsProvider from '@webex/cc-widgets-provider';
import {MyWidget} from './widgets';

function App() {
return (
<WidgetsProvider enableMetrics={true}>
<div data-widget-id="my-custom-widget">
<MyWidget />
</div>
</WidgetsProvider>
);
}

export default App;
```

### Widget Detection

For a widget to be automatically detected and tracked, it must have a `data-widget-id` attribute:

```tsx
// This widget will be automatically detected
<div data-widget-id="user-profile-widget">
<UserProfileWidget />
</div>

// This widget will also be detected
<section data-widget-id="task-list-widget">
<TaskListWidget />
</section>
```

### Accessing Metrics

Use the `useMetrics` hook to access metrics data in your components:

```tsx
import React from 'react';
import {useMetrics} from '@webex/cc-widgets-provider';

function MetricsDashboard() {
const {metrics, sessionSummary, refreshMetrics} = useMetrics();

return (
<div>
<h2>Widget Metrics</h2>
<p>Total Widgets: {sessionSummary.totalWidgetsLoaded}</p>
<p>Active Widgets: {sessionSummary.activeWidgets}</p>

<button onClick={refreshMetrics}>Refresh Metrics</button>

<ul>
{Array.from(metrics.entries()).map(([widgetName, metric]) => (
<li key={widgetName}>
<strong>{widgetName}</strong>: Initialized at {new Date(metric.initializationTime).toLocaleTimeString()}
</li>
))}
</ul>
</div>
);
}
```

## Props

### WidgetsProvider Props

| Prop | Type | Default | Description |
| --------------- | ----------- | ------------------------------- | --------------------------------- |
| `children` | `ReactNode` | Required | Child components to render |
| `enableMetrics` | `boolean` | `true` | Enable/disable metrics collection |
| `themeclass` | `string` | `'mds-theme-stable-lightWebex'` | Momentum Design theme class |
| `iconSet` | `string` | `'momentum-icons'` | Icon set to use |

## Metrics Events

The provider automatically tracks the following events:

- `WIDGET_DETECTED`: When a widget with `data-widget-id` is found in the DOM
- `WIDGET_INITIALIZED`: When a widget is initialized (if using withMetrics HOC)
- `WIDGET_PROP_UPDATED`: When widget props change (if using withMetrics HOC)
- `WIDGET_UNMOUNTED`: When a widget is unmounted (if using withMetrics HOC)

## Manual Widget Tracking (Optional)

If you need more granular control, you can still use the `withMetrics` HOC:

```tsx
import React from 'react';
import {withMetrics} from '@webex/cc-widgets-provider';

const MyWidget = ({title, data}) => {
return (
<div>
<h3>{title}</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};

// Wrap with HOC for detailed tracking
export default withMetrics(MyWidget, 'my-custom-widget');
```

## API Reference

### useMetrics Hook

Returns an object with:

- `metrics`: `Map<string, WidgetMetrics>` - All widget metrics
- `sessionSummary`: Session summary with total and active widget counts
- `refreshMetrics`: Function to manually refresh metrics

### MetricEvent Interface

```typescript
interface MetricEvent {
widgetName: string;
event: 'WIDGET_INITIALIZED' | 'WIDGET_UNMOUNTED' | 'WIDGET_PROP_UPDATED' | 'WIDGET_DETECTED';
props?: any;
timestamp: number;
sessionId?: string;
}
```

### WidgetMetrics Interface

```typescript
interface WidgetMetrics {
widgetName: string;
initializationTime: number;
lastPropUpdate?: number;
unmountTime?: number;
propUpdateCount: number;
currentProps?: any;
}
```

## Example

Here's a complete example of how to use the provider:

```tsx
import React from 'react';
import WidgetsProvider, {useMetrics} from '@webex/cc-widgets-provider';

// A simple widget component
const StatusWidget = ({status}) => <div data-widget-id="status-widget">Status: {status}</div>;

// Component that displays metrics
const MetricsDisplay = () => {
const {metrics, sessionSummary} = useMetrics();

return (
<div>
<h3>Detected Widgets: {sessionSummary.totalWidgetsLoaded}</h3>
{Array.from(metrics.keys()).map((widgetName) => (
<p key={widgetName}>✓ {widgetName}</p>
))}
</div>
);
};

// Main application
function App() {
return (
<WidgetsProvider enableMetrics={true}>
<h1>My Contact Center App</h1>
<StatusWidget status="Online" />
<div data-widget-id="task-panel">
<h2>Task Panel</h2>
<p>Current tasks...</p>
</div>
<MetricsDisplay />
</WidgetsProvider>
);
}

export default App;
```

## Migration from Manual HOC Approach

If you were previously wrapping each widget with `withMetrics`, you can now simply:

1. Remove the HOC wrapping from your widgets
2. Add `data-widget-id` attributes to the widget containers
3. The provider will automatically detect and track them

Before:

```tsx
export default withMetrics(MyWidget, 'my-widget');
```

After:

```tsx
// In your JSX
<div data-widget-id="my-widget">
<MyWidget />
</div>
```

The automatic detection approach is simpler and requires less code changes while providing the same tracking capabilities.
3 changes: 3 additions & 0 deletions packages/contact-center/WidgetsProvider/bable.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const baseConfig = require('../../../babel.config.js');

module.exports = baseConfig;
22 changes: 22 additions & 0 deletions packages/contact-center/WidgetsProvider/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import globals from 'globals';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginReact from 'eslint-plugin-react';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import eslintConfigPrettier from 'eslint-config-prettier';

export default [
{files: ['**/src/**/*.{js,mjs,cjs,ts,jsx,tsx}']},
{
ignores: ['.babelrc.js', '*config.{js,ts}', 'dist', 'node_modules', 'coverage'],
},
{languageOptions: {globals: globals.browser}},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
...pluginReact.configs.flat.recommended,
settings: {react: {version: 'detect'}},
},
eslintPluginPrettierRecommended,
eslintConfigPrettier,
];
6 changes: 6 additions & 0 deletions packages/contact-center/WidgetsProvider/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const jestConfig = require('../../../jest.config.js');

jestConfig.rootDir = '../../../';
jestConfig.testMatch = ['**/station-login/tests/**/*.ts', '**/station-login/tests/**/*.tsx'];

module.exports = jestConfig;
54 changes: 54 additions & 0 deletions packages/contact-center/WidgetsProvider/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@webex/cc-widgets-provider",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"dependencies": {
"@momentum-design/components": "latest",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"peerDependencies": {
"@momentum-design/components": ">=1.0.0",
"react": ">=18.3.1",
"react-dom": ">=18.3.1"
},
"devDependencies": {
"@babel/core": "7.25.2",
"@babel/preset-env": "7.25.4",
"@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.25.9",
"@eslint/js": "^9.20.0",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.2",
"@testing-library/react": "16.0.1",
"@types/jest": "29.5.14",
"@types/node": "^22.13.13",
"@types/react-test-renderer": "18",
"babel-jest": "29.7.0",
"babel-loader": "9.2.1",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.0.1",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.37.4",
"file-loader": "6.2.0",
"globals": "^16.0.0",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"prettier": "^3.5.1",
"ts-loader": "9.5.1",
"typescript": "5.6.3",
"typescript-eslint": "^8.24.1",
"webpack": "5.94.0",
"webpack-cli": "5.1.4",
"webpack-merge": "6.0.1"
},
"scripts": {
"build:src": "webpack",
"dev": "webpack --mode development --watch"
}
}
46 changes: 46 additions & 0 deletions packages/contact-center/WidgetsProvider/src/MetricsContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, {createContext, useContext, useEffect, useState} from 'react';
import {MetricEvent, WidgetMetrics, addMetricsListener, getMetrics, getSessionSummary} from './metricsLogger';

interface MetricsContextType {
metrics: Map<string, WidgetMetrics>;
sessionSummary: ReturnType<typeof getSessionSummary>;
refreshMetrics: () => void;
}

const MetricsContext = createContext<MetricsContextType | undefined>(undefined);

export const useMetrics = () => {
const context = useContext(MetricsContext);
if (!context) {
throw new Error('useMetrics must be used within a MetricsProvider');
}
return context;
};

export const MetricsProvider: React.FC<{children: React.ReactNode}> = ({children}) => {
const [metrics, setMetrics] = useState<Map<string, WidgetMetrics>>(new Map());
const [sessionSummary, setSessionSummary] = useState(getSessionSummary());

const refreshMetrics = React.useCallback(() => {
setMetrics(getMetrics());
setSessionSummary(getSessionSummary());
}, []);

useEffect(() => {
// Listen for metric events and update state
const unsubscribe = addMetricsListener(() => {
setMetrics(getMetrics());
setSessionSummary(getSessionSummary());
});

// Initial load
setMetrics(getMetrics());
setSessionSummary(getSessionSummary());

return unsubscribe;
}, []);

return (
<MetricsContext.Provider value={{metrics, sessionSummary, refreshMetrics}}>{children}</MetricsContext.Provider>
);
};
Loading