@unleash/toolbar 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +599 -0
- package/dist/__tests__/setup.d.ts +5 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.es.js +2 -0
- package/dist/index.es.js.map +1 -0
- package/dist/next/client.d.ts +11 -0
- package/dist/next/client.d.ts.map +1 -0
- package/dist/next/index.d.ts +41 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/server.d.ts +68 -0
- package/dist/next/server.d.ts.map +1 -0
- package/dist/next-server.es.js +2 -0
- package/dist/next-server.es.js.map +1 -0
- package/dist/next.es.js +4 -0
- package/dist/next.es.js.map +1 -0
- package/dist/react/__tests__/index.test.d.ts +2 -0
- package/dist/react/__tests__/index.test.d.ts.map +1 -0
- package/dist/react/hooks.d.ts +8 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/index.d.ts +94 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react.es.js +2 -0
- package/dist/react.es.js.map +1 -0
- package/dist/state.d.ts +90 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/toolbar.css +1 -0
- package/dist/types.d.ts +162 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/ui-LKEosjLO.js +212 -0
- package/dist/ui-LKEosjLO.js.map +1 -0
- package/dist/ui.d.ts +41 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/wrapper.d.ts +16 -0
- package/dist/wrapper.d.ts.map +1 -0
- package/package.json +107 -0
package/README.md
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
# Unleash Toolbar
|
|
2
|
+
|
|
3
|
+
A client-side debugging toolbar for [Unleash](https://www.getunleash.io/) feature flags. Override flag values and context in real-time without server changes.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Flag Overrides**: Force boolean flags ON/OFF or override variant values
|
|
8
|
+
- **Context Overrides**: Modify userId, sessionId, environment, and custom properties
|
|
9
|
+
- **Persistence**: Save overrides in memory, sessionStorage, or localStorage
|
|
10
|
+
- **React Integration**: Seamlessly wraps the official `@unleash/proxy-client-react` SDK
|
|
11
|
+
- **Next.js SSR Support**: Server-side rendering with cookie-based state sync using `@unleash/nextjs`
|
|
12
|
+
- **Customizable UI**: Theming support and positioning options
|
|
13
|
+
- **SDK Compatible**: Works with Unleash JavaScript SDK
|
|
14
|
+
|
|
15
|
+
## Bundle Size
|
|
16
|
+
|
|
17
|
+
The toolbar is optimized for minimal impact on your application:
|
|
18
|
+
|
|
19
|
+
- **Core**: ~8 KB gzipped
|
|
20
|
+
- **React**: ~0.2 KB gzipped (thin wrapper)
|
|
21
|
+
- **Next.js**: ~0.6 KB gzipped (server utilities)
|
|
22
|
+
- **CSS**: ~2.4 KB gzipped
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @unleash/toolbar
|
|
28
|
+
|
|
29
|
+
# For React integration
|
|
30
|
+
npm install @unleash/toolbar @unleash/proxy-client-react unleash-proxy-client
|
|
31
|
+
|
|
32
|
+
# For Next.js SSR integration
|
|
33
|
+
npm install @unleash/toolbar @unleash/nextjs
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
### Vanilla JavaScript
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
import { initUnleashToolbar } from '@unleash/toolbar';
|
|
42
|
+
import { UnleashClient } from 'unleash-proxy-client';
|
|
43
|
+
import '@unleash/toolbar/toolbar.css';
|
|
44
|
+
|
|
45
|
+
// Initialize toolbar with new Unleash client - returns wrapped client
|
|
46
|
+
const client = initUnleashToolbar(new UnleashClient({
|
|
47
|
+
url: 'https://your-unleash-instance.com/api/frontend',
|
|
48
|
+
clientKey: 'your-client-key',
|
|
49
|
+
appName: 'my-app'
|
|
50
|
+
}), {
|
|
51
|
+
storageMode: 'local',
|
|
52
|
+
position: 'bottom-right',
|
|
53
|
+
initiallyVisible: true
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Start the client
|
|
57
|
+
await client.start();
|
|
58
|
+
|
|
59
|
+
// Use the client for all flag evaluations
|
|
60
|
+
const isEnabled = client.isEnabled('my-feature');
|
|
61
|
+
const variant = client.getVariant('my-experiment');
|
|
62
|
+
|
|
63
|
+
// Listen for changes (from toolbar or SDK updates)
|
|
64
|
+
client.on('update', () => {
|
|
65
|
+
// Re-evaluate flags when overrides change
|
|
66
|
+
const newValue = client.isEnabled('my-feature');
|
|
67
|
+
updateUI(newValue);
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### React
|
|
72
|
+
|
|
73
|
+
The toolbar integrates seamlessly with the official **`@unleash/proxy-client-react`** SDK. Just pass your configuration - the toolbar handles everything automatically!
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm install @unleash/toolbar @unleash/proxy-client-react unleash-proxy-client
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { useFlag, useVariant } from '@unleash/proxy-client-react';
|
|
81
|
+
import { UnleashToolbarProvider } from '@unleash/toolbar/react';
|
|
82
|
+
import '@unleash/toolbar/toolbar.css';
|
|
83
|
+
|
|
84
|
+
// Same config format as the official React SDK
|
|
85
|
+
const config = {
|
|
86
|
+
url: 'https://your-unleash-instance.com/api/frontend',
|
|
87
|
+
clientKey: 'your-client-key',
|
|
88
|
+
appName: 'my-app',
|
|
89
|
+
refreshInterval: 15
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// That's it! No need to import FlagProvider or manage the SDK client
|
|
93
|
+
function App() {
|
|
94
|
+
return (
|
|
95
|
+
<UnleashToolbarProvider
|
|
96
|
+
config={config}
|
|
97
|
+
toolbarOptions={{
|
|
98
|
+
storageMode: 'local',
|
|
99
|
+
position: 'bottom-right'
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<MyComponent />
|
|
103
|
+
</UnleashToolbarProvider>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Use hooks from the official React SDK - they work with toolbar overrides!
|
|
108
|
+
function MyComponent() {
|
|
109
|
+
const isEnabled = useFlag('my-feature');
|
|
110
|
+
const variant = useVariant('my-experiment');
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div>
|
|
114
|
+
{isEnabled && <NewFeature />}
|
|
115
|
+
{variant.name === 'variant-a' && <VariantA />}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Key Points:**
|
|
122
|
+
- Pass `config` directly to `UnleashToolbarProvider` - no other imports needed!
|
|
123
|
+
- The toolbar automatically uses `FlagProvider` from the React SDK
|
|
124
|
+
- Import hooks from `@unleash/proxy-client-react` (the official SDK)
|
|
125
|
+
- All official React SDK hooks work seamlessly with toolbar overrides
|
|
126
|
+
|
|
127
|
+
**Advanced: Custom FlagProvider or pre-instantiated client**
|
|
128
|
+
```tsx
|
|
129
|
+
import { FlagProvider } from '@unleash/proxy-client-react';
|
|
130
|
+
import { UnleashClient } from 'unleash-proxy-client';
|
|
131
|
+
|
|
132
|
+
// Option 1: Custom FlagProvider
|
|
133
|
+
<UnleashToolbarProvider
|
|
134
|
+
FlagProvider={FlagProvider} // Optional - use if you need customization
|
|
135
|
+
config={config}
|
|
136
|
+
>
|
|
137
|
+
<MyApp />
|
|
138
|
+
</UnleashToolbarProvider>
|
|
139
|
+
|
|
140
|
+
// Option 2: Pre-instantiated client
|
|
141
|
+
const client = new UnleashClient({ /* config */ });
|
|
142
|
+
|
|
143
|
+
<UnleashToolbarProvider
|
|
144
|
+
client={client} // Pass client instead of config
|
|
145
|
+
toolbarOptions={{ /* ... */ }}
|
|
146
|
+
>
|
|
147
|
+
<MyApp />
|
|
148
|
+
</UnleashToolbarProvider>
|
|
149
|
+
```
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Next.js App Router (Client & Server Components)
|
|
153
|
+
|
|
154
|
+
The toolbar provides full Next.js App Router support with server-side rendering and cookie-based state synchronization.
|
|
155
|
+
|
|
156
|
+
#### Client Components
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
// app/layout.tsx
|
|
160
|
+
import { UnleashToolbarProvider } from '@unleash/toolbar/next';
|
|
161
|
+
import '@unleash/toolbar/toolbar.css';
|
|
162
|
+
|
|
163
|
+
export default function RootLayout({ children }) {
|
|
164
|
+
return (
|
|
165
|
+
<html>
|
|
166
|
+
<body>
|
|
167
|
+
<UnleashToolbarProvider
|
|
168
|
+
config={{
|
|
169
|
+
url: process.env.NEXT_PUBLIC_UNLEASH_URL!,
|
|
170
|
+
clientKey: process.env.NEXT_PUBLIC_UNLEASH_CLIENT_KEY!,
|
|
171
|
+
appName: 'my-next-app',
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{children}
|
|
175
|
+
</UnleashToolbarProvider>
|
|
176
|
+
</body>
|
|
177
|
+
</html>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
// app/page.tsx
|
|
184
|
+
'use client';
|
|
185
|
+
|
|
186
|
+
import { useFlag, useVariant } from '@unleash/toolbar/next';
|
|
187
|
+
|
|
188
|
+
export default function HomePage() {
|
|
189
|
+
const isEnabled = useFlag('new-checkout');
|
|
190
|
+
const variant = useVariant('payment-provider');
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div>
|
|
194
|
+
{isEnabled && <NewCheckout />}
|
|
195
|
+
<PaymentForm provider={variant.name} />
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### Server Components with SSR
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
// app/server-page/page.tsx
|
|
205
|
+
import { cookies } from 'next/headers';
|
|
206
|
+
import { getDefinitions, evaluateFlags, flagsClient } from '@unleash/nextjs';
|
|
207
|
+
import { applyToolbarOverrides } from '@unleash/toolbar/next/server';
|
|
208
|
+
|
|
209
|
+
export default async function ServerPage() {
|
|
210
|
+
// Fetch definitions from Unleash API (uses env config)
|
|
211
|
+
const definitions = await getDefinitions({
|
|
212
|
+
fetchOptions: { next: { revalidate: 15 } },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Apply toolbar overrides from cookies
|
|
216
|
+
const cookieStore = await cookies();
|
|
217
|
+
const modifiedDefinitions = applyToolbarOverrides(definitions, cookieStore);
|
|
218
|
+
|
|
219
|
+
// Evaluate flags with context
|
|
220
|
+
const { toggles } = evaluateFlags(modifiedDefinitions, {
|
|
221
|
+
sessionId: 'session-id',
|
|
222
|
+
userId: 'user-id',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Create offline client
|
|
226
|
+
const flags = flagsClient(toggles);
|
|
227
|
+
|
|
228
|
+
// Check flags server-side
|
|
229
|
+
const isEnabled = flags.isEnabled('new-feature');
|
|
230
|
+
|
|
231
|
+
return <div>{isEnabled ? 'Feature ON' : 'Feature OFF'}</div>;
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Environment Variables for Next.js:**
|
|
236
|
+
```env
|
|
237
|
+
# Used by @unleash/nextjs SDK
|
|
238
|
+
UNLEASH_SERVER_API_URL=https://your-unleash-instance.com/api
|
|
239
|
+
UNLEASH_SERVER_API_TOKEN=your-server-token
|
|
240
|
+
UNLEASH_APP_NAME=my-app
|
|
241
|
+
|
|
242
|
+
# Used by client-side toolbar
|
|
243
|
+
NEXT_PUBLIC_UNLEASH_URL=https://your-unleash-instance.com/api/frontend
|
|
244
|
+
NEXT_PUBLIC_UNLEASH_CLIENT_KEY=your-client-key
|
|
245
|
+
NEXT_PUBLIC_UNLEASH_APP_NAME=my-app
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**How it works:**
|
|
249
|
+
1. Client-side toolbar automatically syncs state to cookies
|
|
250
|
+
2. Server components read toolbar state from cookies
|
|
251
|
+
3. `applyToolbarOverrides()` modifies flag definitions before evaluation
|
|
252
|
+
4. Flags evaluate server-side with toolbar overrides applied
|
|
253
|
+
5. Changes in toolbar immediately affect both client and server rendering
|
|
254
|
+
|
|
255
|
+
### Vue 3 (Composition API)
|
|
256
|
+
|
|
257
|
+
```vue
|
|
258
|
+
<script setup lang="ts">
|
|
259
|
+
import { ref, watch } from 'vue'
|
|
260
|
+
import { useUnleash } from './composables/useUnleash'
|
|
261
|
+
|
|
262
|
+
// Initialize Unleash with the composable
|
|
263
|
+
const { unleashClient, isReady, updateTrigger } = useUnleash()
|
|
264
|
+
|
|
265
|
+
// Reactive flag states
|
|
266
|
+
const isEnabled = ref(false)
|
|
267
|
+
|
|
268
|
+
// Evaluate flags
|
|
269
|
+
const evaluateFlags = () => {
|
|
270
|
+
if (!unleashClient.value) return
|
|
271
|
+
isEnabled.value = unleashClient.value.isEnabled('my-feature')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Evaluate when ready
|
|
275
|
+
watch(isReady, (ready) => {
|
|
276
|
+
if (ready) evaluateFlags()
|
|
277
|
+
})
|
|
278
|
+
</script>
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Configuration Options
|
|
282
|
+
|
|
283
|
+
### `initUnleashToolbar(client, options)`
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
interface InitToolbarOptions {
|
|
287
|
+
// Persistence mode (default: 'local')
|
|
288
|
+
// - 'local': Persists across tabs and browser restarts (RECOMMENDED for development)
|
|
289
|
+
// - 'session': Persists only in current tab, cleared when tab closes
|
|
290
|
+
// - 'memory': No persistence, cleared on page reload
|
|
291
|
+
storageMode?: 'memory' | 'session' | 'local';
|
|
292
|
+
|
|
293
|
+
// Storage key for persistence (default: 'unleash-toolbar-state')
|
|
294
|
+
storageKey?: string;
|
|
295
|
+
|
|
296
|
+
// UI position (default: 'bottom-right')
|
|
297
|
+
// Corner positions: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
|
|
298
|
+
// Side positions (vertically centered): 'left' | 'right'
|
|
299
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'left' | 'right';
|
|
300
|
+
|
|
301
|
+
// Initial visibility (default: false, but respects persisted state if available)
|
|
302
|
+
initiallyVisible?: boolean;
|
|
303
|
+
|
|
304
|
+
// Sort flags alphabetically instead of by evaluation order (default: false)
|
|
305
|
+
sortAlphabetically?: boolean;
|
|
306
|
+
|
|
307
|
+
// Theme preset (default: 'light')
|
|
308
|
+
themePreset?: 'light' | 'dark';
|
|
309
|
+
|
|
310
|
+
// Theme customization (overrides preset)
|
|
311
|
+
theme?: {
|
|
312
|
+
primaryColor?: string;
|
|
313
|
+
backgroundColor?: string;
|
|
314
|
+
textColor?: string;
|
|
315
|
+
borderColor?: string;
|
|
316
|
+
fontFamily?: string;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Custom container element (default: document.body)
|
|
320
|
+
container?: HTMLElement | null;
|
|
321
|
+
|
|
322
|
+
// Enable cookie sync for SSR (default: false)
|
|
323
|
+
// Set to true for Next.js or other SSR frameworks
|
|
324
|
+
enableCookieSync?: boolean;
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Next.js Server Utilities
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import { applyToolbarOverrides, applyToolbarOverridesToToggles, getToolbarStateFromCookies } from '@unleash/toolbar/next/server';
|
|
332
|
+
import type { ClientFeaturesResponse, IToggle } from '@unleash/nextjs';
|
|
333
|
+
|
|
334
|
+
// Apply overrides before evaluation (recommended)
|
|
335
|
+
const modifiedDefinitions = applyToolbarOverrides(
|
|
336
|
+
definitions: ClientFeaturesResponse,
|
|
337
|
+
cookieStore: CookieStore
|
|
338
|
+
): ClientFeaturesResponse;
|
|
339
|
+
|
|
340
|
+
// Apply overrides after evaluation (alternative)
|
|
341
|
+
const modifiedToggles = applyToolbarOverridesToToggles(
|
|
342
|
+
toggles: IToggle[],
|
|
343
|
+
cookieStore: CookieStore
|
|
344
|
+
): IToggle[];
|
|
345
|
+
|
|
346
|
+
// Read toolbar state from cookies
|
|
347
|
+
const state = getToolbarStateFromCookies(
|
|
348
|
+
cookieStore: CookieStore
|
|
349
|
+
): ToolbarState | null;
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Storage Modes Explained
|
|
353
|
+
|
|
354
|
+
**`local` (default)**: Best for development workflows
|
|
355
|
+
- ✅ Persists across all browser tabs
|
|
356
|
+
- ✅ Survives page reloads and browser restarts
|
|
357
|
+
- ✅ Set overrides once, test everywhere
|
|
358
|
+
- Use case: Daily feature development and debugging
|
|
359
|
+
|
|
360
|
+
**`session`**: Useful for isolated testing
|
|
361
|
+
- ✅ Persists within current tab only
|
|
362
|
+
- ✅ Survives page reloads in the same tab
|
|
363
|
+
- ❌ Lost when tab is closed
|
|
364
|
+
- Use case: Testing different configurations in multiple tabs simultaneously
|
|
365
|
+
|
|
366
|
+
**`memory`**: Temporary testing only
|
|
367
|
+
- ❌ Lost on every page reload
|
|
368
|
+
- ❌ No persistence whatsoever
|
|
369
|
+
- Use case: Quick one-off tests or strict security requirements
|
|
370
|
+
|
|
371
|
+
## API Reference
|
|
372
|
+
|
|
373
|
+
### Toolbar Instance
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
const toolbar = window.unleashToolbar;
|
|
377
|
+
|
|
378
|
+
// Show/hide the toolbar
|
|
379
|
+
toolbar.show();
|
|
380
|
+
toolbar.hide();
|
|
381
|
+
|
|
382
|
+
// Get current state
|
|
383
|
+
const state = toolbar.getState();
|
|
384
|
+
|
|
385
|
+
// Set flag overrides
|
|
386
|
+
toolbar.setFlagOverride('my-feature', { type: 'flag', value: true });
|
|
387
|
+
toolbar.setFlagOverride('my-variant', { type: 'variant', variantKey: 'variant-b' });
|
|
388
|
+
toolbar.setFlagOverride('my-feature', null); // Clear override
|
|
389
|
+
|
|
390
|
+
// Set context overrides
|
|
391
|
+
toolbar.setContextOverride({
|
|
392
|
+
userId: 'test-user-123',
|
|
393
|
+
properties: { tier: 'premium' }
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Reset overrides
|
|
397
|
+
toolbar.resetOverrides();
|
|
398
|
+
toolbar.resetContextOverrides();
|
|
399
|
+
|
|
400
|
+
// Cleanup
|
|
401
|
+
toolbar.destroy();
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Listening to Changes
|
|
405
|
+
|
|
406
|
+
The toolbar automatically triggers the Unleash SDK's `'update'` event when overrides change. Use the standard SDK pattern to listen for changes:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
// Listen to flag/context changes from the toolbar
|
|
410
|
+
client.on('update', () => {
|
|
411
|
+
console.log('Flags updated - re-evaluate your flags');
|
|
412
|
+
// Re-render your UI, re-check flags, etc.
|
|
413
|
+
});
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
This works for both toolbar changes (flag overrides, context overrides) and SDK changes (new config from server).
|
|
417
|
+
|
|
418
|
+
### React Hooks
|
|
419
|
+
|
|
420
|
+
Use hooks from the official `@unleash/proxy-client-react` SDK - they automatically work with toolbar overrides!
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
import {
|
|
424
|
+
useFlag,
|
|
425
|
+
useVariant,
|
|
426
|
+
useUnleashClient,
|
|
427
|
+
useUnleashContext,
|
|
428
|
+
useFlagsStatus
|
|
429
|
+
} from '@unleash/proxy-client-react';
|
|
430
|
+
|
|
431
|
+
// Check flag status - updates automatically when toolbar overrides change
|
|
432
|
+
const isEnabled = useFlag('my-feature');
|
|
433
|
+
|
|
434
|
+
// Get variant - updates automatically when toolbar overrides change
|
|
435
|
+
const variant = useVariant('my-experiment');
|
|
436
|
+
|
|
437
|
+
// Access client (the wrapped client from the toolbar)
|
|
438
|
+
const client = useUnleashClient();
|
|
439
|
+
|
|
440
|
+
// Update context dynamically
|
|
441
|
+
const updateContext = useUnleashContext();
|
|
442
|
+
await updateContext({ userId: 'new-user-id' });
|
|
443
|
+
|
|
444
|
+
// Check loading/ready state
|
|
445
|
+
const { flagsReady, flagsError } = useFlagsStatus();
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Next.js Hooks
|
|
449
|
+
|
|
450
|
+
For Next.js, import hooks from `@unleash/toolbar/next`, they're re-exported for convenience:
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
import { useFlag, useVariant, useUnleashClient } from '@unleash/toolbar/next';
|
|
454
|
+
|
|
455
|
+
const isEnabled = useFlag('my-feature');
|
|
456
|
+
const variant = useVariant('my-experiment');
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## UI Features
|
|
460
|
+
|
|
461
|
+
### Flag List Tab
|
|
462
|
+
|
|
463
|
+
- **Override Controls**: Dropdown to set boolean or variant overrides
|
|
464
|
+
- **Flag Info**: See default vs. effective values for each flag
|
|
465
|
+
- **Quick Actions**: Reset individual flag overrides
|
|
466
|
+
|
|
467
|
+
### Context Tab
|
|
468
|
+
|
|
469
|
+
- **Standard Fields**: userId, sessionId, remoteAddress, environment, appName
|
|
470
|
+
- **Custom Properties**: Edit or reset property values
|
|
471
|
+
- **Live Updates**: Changes apply immediately to all evaluations
|
|
472
|
+
|
|
473
|
+
### Header Actions
|
|
474
|
+
|
|
475
|
+
- **Reset Flags**: Clear all flag overrides
|
|
476
|
+
- **Reset Context**: Clear all context overrides
|
|
477
|
+
- **Close**: Hide the toolbar
|
|
478
|
+
|
|
479
|
+
## Theme Customization
|
|
480
|
+
|
|
481
|
+
Override CSS variables or use the theme option:
|
|
482
|
+
|
|
483
|
+
```javascript
|
|
484
|
+
const toolbar = initUnleashSessionToolbar({
|
|
485
|
+
theme: {
|
|
486
|
+
primaryColor: '#ff6b6b',
|
|
487
|
+
backgroundColor: '#ffffff',
|
|
488
|
+
textColor: '#2d3436',
|
|
489
|
+
borderColor: '#dfe6e9',
|
|
490
|
+
fontFamily: 'Inter, sans-serif'
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Or override CSS variables globally:
|
|
496
|
+
|
|
497
|
+
```css
|
|
498
|
+
:root {
|
|
499
|
+
--unleash-toolbar-primary: #your-color;
|
|
500
|
+
--unleash-toolbar-bg: #your-bg;
|
|
501
|
+
--unleash-toolbar-text: #your-text;
|
|
502
|
+
--unleash-toolbar-border: #your-border;
|
|
503
|
+
--unleash-toolbar-font: your-font;
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
## Development
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
# Install dependencies
|
|
511
|
+
npm install
|
|
512
|
+
|
|
513
|
+
# Build the library
|
|
514
|
+
npm run build
|
|
515
|
+
|
|
516
|
+
# Run tests
|
|
517
|
+
npm test
|
|
518
|
+
|
|
519
|
+
# Run tests in watch mode
|
|
520
|
+
npm run test:watch
|
|
521
|
+
|
|
522
|
+
# Run tests with coverage
|
|
523
|
+
npm run test:coverage
|
|
524
|
+
|
|
525
|
+
# Run type checking
|
|
526
|
+
npm run type-check
|
|
527
|
+
|
|
528
|
+
# Run linter
|
|
529
|
+
npm run lint
|
|
530
|
+
|
|
531
|
+
# Auto-fix linting issues
|
|
532
|
+
npm run lint:fix
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Example Applications
|
|
536
|
+
|
|
537
|
+
The repository includes example applications demonstrating integration with different frameworks:
|
|
538
|
+
|
|
539
|
+
- **Vanilla JS** - Basic HTML/JavaScript integration
|
|
540
|
+
```bash
|
|
541
|
+
npm run serve:example:vanilla
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
- **React** - Hooks and provider pattern with official React SDK
|
|
545
|
+
```bash
|
|
546
|
+
npm run serve:example:react
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
- **Next.js App Router** - Server-side rendering with client and server components
|
|
550
|
+
```bash
|
|
551
|
+
npm run serve:example:nextjs
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
- **Angular** - Service-based integration
|
|
555
|
+
```bash
|
|
556
|
+
npm run serve:example:angular
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
- **Vue 3** - Composition API with composables
|
|
560
|
+
```bash
|
|
561
|
+
npm run serve:example:vue
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
All examples include:
|
|
565
|
+
- Environment configuration setup
|
|
566
|
+
- Multiple feature flags for testing
|
|
567
|
+
- Variant flag demonstrations
|
|
568
|
+
- Toolbar integration best practices
|
|
569
|
+
- Server-side rendering examples (Next.js)
|
|
570
|
+
|
|
571
|
+
## Requirements
|
|
572
|
+
|
|
573
|
+
- **Browser**: Modern browsers with ES2020 support (Chrome 90+, Firefox 88+, Safari 14+)
|
|
574
|
+
- **Unleash SDKs**:
|
|
575
|
+
- `unleash-proxy-client` ^3.0.0 (required)
|
|
576
|
+
- `@unleash/proxy-client-react` ^5.0.0 (optional, for React)
|
|
577
|
+
- `@unleash/nextjs` ^1.0.0 (optional, for Next.js SSR)
|
|
578
|
+
|
|
579
|
+
## Contributing
|
|
580
|
+
|
|
581
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
582
|
+
|
|
583
|
+
## License
|
|
584
|
+
|
|
585
|
+
Apache-2.0
|
|
586
|
+
|
|
587
|
+
## Links
|
|
588
|
+
|
|
589
|
+
- [Unleash Documentation](https://docs.getunleash.io/)
|
|
590
|
+
- [Unleash JavaScript SDK](https://github.com/Unleash/unleash-proxy-client-js)
|
|
591
|
+
- [Unleash React SDK](https://github.com/Unleash/proxy-client-react)
|
|
592
|
+
- [Unleash Next.js SDK](https://github.com/Unleash/unleash-nextjs-sdk)
|
|
593
|
+
- [npm Package](https://www.npmjs.com/package/@unleash/toolbar)
|
|
594
|
+
|
|
595
|
+
## Support
|
|
596
|
+
|
|
597
|
+
- [GitHub Issues](https://github.com/Unleash/toolbar/issues)
|
|
598
|
+
- [Unleash Slack Community](https://unleash-community.slack.com/)
|
|
599
|
+
- [Unleash Documentation](https://docs.getunleash.io/)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/__tests__/setup.ts"],"names":[],"mappings":"AAGA,UAAU,aAAa;IACrB,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACjC;AAED,QAAA,MAAM,CAAC,EAA4B,aAAa,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { UnleashClient } from 'unleash-proxy-client';
|
|
2
|
+
import { ToolbarStateManager } from './state';
|
|
3
|
+
import { FlagOverride, InitToolbarOptions, ToolbarState, UnleashContext, UnleashToolbarInstance, WrappedUnleashClient } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Main toolbar instance implementation
|
|
6
|
+
*/
|
|
7
|
+
export declare class UnleashToolbar implements UnleashToolbarInstance {
|
|
8
|
+
private stateManager;
|
|
9
|
+
private ui;
|
|
10
|
+
readonly client: WrappedUnleashClient;
|
|
11
|
+
constructor(stateManager: ToolbarStateManager, wrappedClient: WrappedUnleashClient, options: InitToolbarOptions);
|
|
12
|
+
private initUI;
|
|
13
|
+
show(): void;
|
|
14
|
+
hide(): void;
|
|
15
|
+
destroy(): void;
|
|
16
|
+
getState(): ToolbarState;
|
|
17
|
+
getFlagNames(): string[];
|
|
18
|
+
setFlagOverride(name: string, override: FlagOverride | null): void;
|
|
19
|
+
setContextOverride(context: Partial<UnleashContext>): void;
|
|
20
|
+
removeContextOverride(fieldName: keyof UnleashContext): void;
|
|
21
|
+
resetOverrides(): void;
|
|
22
|
+
resetContextOverrides(): void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Initialize the Unleash Toolbar with a client
|
|
26
|
+
* This is the main entry point - handles both toolbar creation and client wrapping
|
|
27
|
+
* Returns the wrapped client directly for immediate use
|
|
28
|
+
*
|
|
29
|
+
* @param client - The Unleash client to wrap
|
|
30
|
+
* @param options - Toolbar configuration options
|
|
31
|
+
*/
|
|
32
|
+
export declare function initUnleashToolbar(client: UnleashClient, options?: InitToolbarOptions): WrappedUnleashClient;
|
|
33
|
+
export { ToolbarStateManager } from './state';
|
|
34
|
+
export * from './types';
|
|
35
|
+
export { wrapUnleashClient } from './wrapper';
|
|
36
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAElB,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACrB,MAAM,SAAS,CAAC;AAGjB;;GAEG;AACH,qBAAa,cAAe,YAAW,sBAAsB;IAC3D,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,EAAE,CAAoB;IAC9B,SAAgB,MAAM,EAAE,oBAAoB,CAAC;gBAG3C,YAAY,EAAE,mBAAmB,EACjC,aAAa,EAAE,oBAAoB,EACnC,OAAO,EAAE,kBAAkB;YAUf,MAAM;IASpB,IAAI,IAAI,IAAI;IAIZ,IAAI,IAAI,IAAI;IAIZ,OAAO,IAAI,IAAI;IAKf,QAAQ,IAAI,YAAY;IAIxB,YAAY,IAAI,MAAM,EAAE;IAIxB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI;IAIlE,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,IAAI;IAI1D,qBAAqB,CAAC,SAAS,EAAE,MAAM,cAAc,GAAG,IAAI;IAI5D,cAAc,IAAI,IAAI;IAItB,qBAAqB,IAAI,IAAI;CAG9B;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,aAAa,EACrB,OAAO,GAAE,kBAAuB,GAC/B,oBAAoB,CAuBtB;AAUD,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAE9C,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/index.es.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
class e{listeners=/* @__PURE__ */new Set;subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}emit(e){this.listeners.forEach(t=>{try{t(e)}catch(a){console.error("[Unleash Toolbar] Error in event listener:",a)}})}}class t{constructor(e,t){this.mode=e,this.key=t}enableCookieSync=!1;setCookieSyncEnabled(e){this.enableCookieSync=e}getStorage(){if("undefined"==typeof window)return null;switch(this.mode){case"local":return window.localStorage;case"session":return window.sessionStorage;default:return null}}syncToCookies(e){if(this.enableCookieSync&&"undefined"!=typeof document)try{!async function(e,t){if("undefined"==typeof document)return;const a=encodeURIComponent(t);if("cookieStore"in window)try{return void(await window.cookieStore.set({name:e,value:a,path:"/",sameSite:"lax",expires:Date.now()+6048e5}))}catch{}document.cookie=[`${e}=${a}`,"path=/","max-age=604800","SameSite=Lax"].join("; ")}("unleash-toolbar-state",JSON.stringify(e))}catch(t){console.error("[Unleash Toolbar] Failed to sync state to cookies:",t)}}load(){const e=this.getStorage();if(!e)return null;try{const t=e.getItem(this.key);return t?JSON.parse(t):null}catch(t){return console.error("[Unleash Toolbar] Failed to load state from storage:",t),null}}save(e){const t=this.getStorage();if(t)try{t.setItem(this.key,JSON.stringify(e)),this.syncToCookies(e)}catch(a){console.error("[Unleash Toolbar] Failed to save state to storage:",a)}}clear(){const e=this.getStorage();if(e)try{e.removeItem(this.key),this.enableCookieSync&&async function(e){if("undefined"!=typeof document){if("cookieStore"in window)try{return void(await window.cookieStore.delete(e))}catch{}document.cookie=`${e}=; path=/; max-age=0`}}("unleash-toolbar-state")}catch(t){console.error("[Unleash Toolbar] Failed to clear storage:",t)}}}class a{state;eventEmitter;storage;sortAlphabetically;constructor(a="local",s="unleash-toolbar-state",r=!1){this.eventEmitter=new e,this.storage=new t(a,s),this.sortAlphabetically=r;const i=this.storage.load();this.state=i||this.getInitialState()}getInitialState(){return{flags:{},contextOverrides:{}}}persist(){this.storage.save(this.state)}applyOverride(e,t){return t?"flag"===t.type?t.value:"variant"===t.type?"object"==typeof e&&null!==e&&"name"in e?{...e,name:t.variantKey,enabled:!0}:{name:t.variantKey,enabled:!0}:t:e}reEvaluateAllFlags(e){Object.keys(this.state.flags).forEach(t=>{const a=this.state.flags[t];if(a)try{const{defaultValue:s,effectiveValue:r}=e(t);this.state.flags[t]={...a,lastDefaultValue:s,lastEffectiveValue:r}}catch(s){console.error(`[Unleash Toolbar] Failed to re-evaluate flag ${t}:`,s)}}),this.persist(),this.eventEmitter.emit({type:"sdk_updated",timestamp:Date.now()})}getState(){return JSON.parse(JSON.stringify(this.state))}subscribe(e){return this.eventEmitter.subscribe(e)}recordEvaluation(e,t,a,s,r){const i=this.state.flags[e],n=!i;this.state.flags[e]={flagType:t,lastDefaultValue:a,lastEffectiveValue:s,lastContext:r,override:i?.override||null},this.persist(),n&&this.eventEmitter.emit({type:"sdk_updated",timestamp:Date.now()})}setFlagOverride(e,t){this.state.flags[e]?(this.state.flags[e].override=t,this.state.flags[e].lastEffectiveValue=this.applyOverride(this.state.flags[e].lastDefaultValue,t)):this.state.flags[e]={flagType:"variant"===t?.type?"variant":"flag",lastDefaultValue:null,lastEffectiveValue:this.applyOverride(null,t),lastContext:null,override:t},this.persist(),this.eventEmitter.emit({type:"flag_override_changed",name:e,override:t,timestamp:Date.now()})}getFlagOverride(e){return this.state.flags[e]?.override||null}setContextOverride(e){this.state.contextOverrides={...this.state.contextOverrides,...e},this.persist(),this.eventEmitter.emit({type:"context_override_changed",contextOverrides:this.state.contextOverrides})}removeContextOverride(e){const t={...this.state.contextOverrides};delete t[e],this.state.contextOverrides=t,this.persist(),this.eventEmitter.emit({type:"context_override_changed",contextOverrides:this.state.contextOverrides})}getMergedContext(e={}){return{...e,...this.state.contextOverrides,properties:{...e.properties||{},...this.state.contextOverrides.properties||{}}}}resetOverrides(){Object.keys(this.state.flags).forEach(e=>{this.state.flags[e].override=null,this.state.flags[e].lastEffectiveValue=this.state.flags[e].lastDefaultValue,this.eventEmitter.emit({type:"flag_override_changed",name:e,override:null,timestamp:Date.now()})}),this.persist()}resetContextOverrides(){this.state.contextOverrides={},this.persist(),this.eventEmitter.emit({type:"context_override_changed",contextOverrides:{}})}setVisibility(e){this.state.isVisible=e,this.persist()}getVisibility(){return this.state.isVisible}clearPersistence(){this.storage.clear()}getFlagNames(){const e=Object.keys(this.state.flags);return this.sortAlphabetically?e.sort():e}getFlagMetadata(e){return this.state.flags[e]||null}enableCookieSync(){this.storage.setCookieSyncEnabled(!0)}}function s(e,t){if("__original"in e)return e;const a=e.getContext(),s=/* @__PURE__ */new Set;let i;const n={__original:e,isEnabled:function(a){const s=e.getContext(),i=t.getMergedContext(s),n=e.isEnabled(a),o=r(n,t.getFlagOverride(a));return t.recordEvaluation(a,"flag",n,o,i),o},getVariant:function(a){const s=e.getContext(),i=t.getMergedContext(s),n=e.getVariant(a),o=r(n,t.getFlagOverride(a));return t.recordEvaluation(a,"variant",n,o,i),o},getContext:function(){const a=e.getContext();return{appName:"",...t.getMergedContext(a)}},on:function(t,a){return"update"===t&&s.add(a),e.on(t,a),i}};function o(){s.forEach(e=>{try{e()}catch(t){console.error("[Unleash Toolbar] Error in update listener:",t)}})}return e.on("update",()=>{t.reEvaluateAllFlags(a=>{const s=t.getFlagMetadata(a),i="variant"===s?.flagType?e.getVariant(a):e.isEnabled(a);return{defaultValue:i,effectiveValue:r(i,t.getFlagOverride(a))}})}),t.subscribe(s=>{if("context_override_changed"===s.type){const s=t.getMergedContext(a),{appName:i,environment:n,...l}=s;e.updateContext(l).then(()=>{t.reEvaluateAllFlags(a=>{const s=t.getFlagMetadata(a),i="variant"===s?.flagType?e.getVariant(a):e.isEnabled(a);return{defaultValue:i,effectiveValue:r(i,t.getFlagOverride(a))}}),o()}).catch(e=>{console.error("[Unleash Toolbar] Failed to update context:",e)})}"flag_override_changed"===s.type&&o()}),i=new Proxy(n,{get(t,a){if(a in t)return Reflect.get(t,a);const s=Reflect.get(e,a);return"function"==typeof s?s.bind(e):s},set:(t,a,s)=>a in t?(Reflect.set(t,a,s),!0):(Reflect.set(e,a,s),!0)}),i}function r(e,t){return t?"flag"===t.type?t.value:"variant"===t.type?"object"==typeof e&&null!==e&&"name"in e?{...e,name:t.variantKey,enabled:!0}:{name:t.variantKey,enabled:!0}:e:e}class i{stateManager;ui;client;constructor(e,t,a){this.stateManager=e,this.client=t,this.ui=null,this.initUI(e,t,a)}async initUI(e,t,a){const{ToolbarUI:s}=await import("./ui-LKEosjLO.js");this.ui=new s(e,t,a)}show(){this.ui&&this.ui.show()}hide(){this.ui&&this.ui.hide()}destroy(){this.ui&&this.ui.destroy(),this.stateManager.clearPersistence()}getState(){return this.stateManager.getState()}getFlagNames(){return this.stateManager.getFlagNames()}setFlagOverride(e,t){this.stateManager.setFlagOverride(e,t)}setContextOverride(e){this.stateManager.setContextOverride(e)}removeContextOverride(e){this.stateManager.removeContextOverride(e)}resetOverrides(){this.stateManager.resetOverrides()}resetContextOverrides(){this.stateManager.resetContextOverrides()}}function n(e,t={}){const r=t.enableCookieSync||!1,n=new a(t.storageMode||"local",t.storageKey||"unleash-toolbar-state",t.sortAlphabetically||!1);r&&n.enableCookieSync();const o=s(e,n),l=new i(n,o,t);return"undefined"!=typeof window&&(window.unleashToolbar=l),o}"undefined"!=typeof window&&(window.UnleashToolbar={init:n});export{a as ToolbarStateManager,i as UnleashToolbar,n as initUnleashToolbar,s as wrapUnleashClient};
|
|
2
|
+
//# sourceMappingURL=index.es.js.map
|