@odigos/ui-kit 0.0.17 → 0.0.18

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.
@@ -0,0 +1,563 @@
1
+ import { ActionType, StatusType, AddNodeTypes, EntityTypes, FieldTypes } from './types.js';
2
+ import { useState, useEffect, useCallback } from 'react';
3
+ import { f as useNotificationStore, e as useModalStore, a as useDrawerStore, b as useEntityStore, i as useSetupStore } from './index-Bdimyacn.js';
4
+ import 'styled-components';
5
+ import { i as isEmpty, s as safeJsonParse } from './index-BZS1ijMm.js';
6
+ import './index-jPxFCX-5.js';
7
+ import { F as FORM_ALERTS } from './index-C_0J5P9M.js';
8
+ import { b as useGenericForm } from './useTransition-WRhgkuG2.js';
9
+ import { g as getIdFromSseTarget } from './index-7-KCQK-x.js';
10
+
11
+ const INITIAL$2 = {
12
+ // @ts-ignore (TS complains about empty string because we expect an "ActionsType", but it's fine)
13
+ type: '',
14
+ name: '',
15
+ notes: '',
16
+ signals: [],
17
+ disabled: false,
18
+ clusterAttributes: null,
19
+ renames: null,
20
+ attributeNamesToDelete: null,
21
+ piiCategories: null,
22
+ fallbackSamplingRatio: null,
23
+ samplingPercentage: null,
24
+ endpointsFilters: null,
25
+ };
26
+ const useActionFormData = () => {
27
+ const { addNotification } = useNotificationStore();
28
+ const { formData, formErrors, handleFormChange, handleErrorChange, resetFormData } = useGenericForm(INITIAL$2);
29
+ const validateForm = (params) => {
30
+ const errors = {};
31
+ let ok = true;
32
+ Object.entries(formData).forEach(([k, v]) => {
33
+ switch (k) {
34
+ case 'type':
35
+ case 'signals':
36
+ if (isEmpty(v))
37
+ errors[k] = FORM_ALERTS.FIELD_IS_REQUIRED;
38
+ break;
39
+ case 'clusterAttributes':
40
+ if (formData.type === ActionType.AddClusterInfo && isEmpty(v))
41
+ errors[k] = FORM_ALERTS.FIELD_IS_REQUIRED;
42
+ break;
43
+ case 'renames':
44
+ if (formData.type === ActionType.RenameAttributes && isEmpty(v))
45
+ errors[k] = FORM_ALERTS.FIELD_IS_REQUIRED;
46
+ break;
47
+ case 'attributeNamesToDelete':
48
+ if (formData.type === ActionType.DeleteAttributes && isEmpty(v))
49
+ errors[k] = FORM_ALERTS.FIELD_IS_REQUIRED;
50
+ break;
51
+ case 'piiCategories':
52
+ if (formData.type === ActionType.PiiMasking && isEmpty(v))
53
+ errors[k] = FORM_ALERTS.FIELD_IS_REQUIRED;
54
+ break;
55
+ case 'fallbackSamplingRatio':
56
+ if (formData.type === ActionType.ErrorSampler && isEmpty(v))
57
+ errors[k] = FORM_ALERTS.FIELD_IS_REQUIRED;
58
+ break;
59
+ case 'samplingPercentage':
60
+ if (formData.type === ActionType.ProbabilisticSampler && isEmpty(v))
61
+ errors[k] = FORM_ALERTS.FIELD_IS_REQUIRED;
62
+ break;
63
+ case 'endpointsFilters':
64
+ if (formData.type === ActionType.LatencySampler) {
65
+ if (isEmpty(v))
66
+ errors[k] = FORM_ALERTS.FIELD_IS_REQUIRED;
67
+ v?.forEach((endpoint) => {
68
+ if (endpoint.httpRoute.charAt(0) !== '/')
69
+ errors[k] = FORM_ALERTS.LATENCY_HTTP_ROUTE;
70
+ });
71
+ }
72
+ break;
73
+ }
74
+ });
75
+ ok = !Object.values(errors).length;
76
+ if (!ok && params?.withAlert) {
77
+ addNotification({
78
+ type: StatusType.Warning,
79
+ title: params.alertTitle,
80
+ message: FORM_ALERTS.REQUIRED_FIELDS,
81
+ hideFromHistory: true,
82
+ });
83
+ }
84
+ handleErrorChange(undefined, undefined, errors);
85
+ return ok;
86
+ };
87
+ const loadFormWithDrawerItem = ({ type, spec }) => {
88
+ const updatedData = {
89
+ ...INITIAL$2,
90
+ type,
91
+ };
92
+ Object.entries(spec).forEach(([k, v]) => {
93
+ if (!!v) {
94
+ switch (k) {
95
+ case 'actionName': {
96
+ updatedData['name'] = v;
97
+ break;
98
+ }
99
+ case 'type':
100
+ case 'notes':
101
+ case 'signals':
102
+ case 'disabled':
103
+ case 'collectContainerAttributes':
104
+ case 'collectWorkloadId':
105
+ case 'collectClusterId':
106
+ case 'labelsAttributes':
107
+ case 'annotationsAttributes':
108
+ case 'clusterAttributes':
109
+ case 'attributeNamesToDelete':
110
+ case 'renames':
111
+ case 'piiCategories':
112
+ case 'fallbackSamplingRatio':
113
+ case 'samplingPercentage':
114
+ case 'endpointsFilters': {
115
+ // @ts-ignore
116
+ updatedData[k] = v;
117
+ break;
118
+ }
119
+ }
120
+ }
121
+ });
122
+ handleFormChange(undefined, undefined, updatedData);
123
+ };
124
+ return {
125
+ formData,
126
+ formErrors,
127
+ handleFormChange,
128
+ resetFormData,
129
+ validateForm,
130
+ loadFormWithDrawerItem,
131
+ };
132
+ };
133
+
134
+ const useClickNode = () => {
135
+ const { setCurrentModal } = useModalStore();
136
+ const { setDrawerType, setDrawerEntityId } = useDrawerStore();
137
+ const onClickNode = (_, object) => {
138
+ const { data: { id, type }, } = object;
139
+ switch (type) {
140
+ case EntityTypes.Source:
141
+ case EntityTypes.Destination:
142
+ case EntityTypes.Action:
143
+ case EntityTypes.InstrumentationRule:
144
+ setDrawerType(type);
145
+ setDrawerEntityId(id);
146
+ break;
147
+ case AddNodeTypes.AddSource:
148
+ setCurrentModal(EntityTypes.Source);
149
+ break;
150
+ case AddNodeTypes.AddDestination:
151
+ setCurrentModal(EntityTypes.Destination);
152
+ break;
153
+ case AddNodeTypes.AddAction:
154
+ setCurrentModal(EntityTypes.Action);
155
+ break;
156
+ case AddNodeTypes.AddRule:
157
+ setCurrentModal(EntityTypes.InstrumentationRule);
158
+ break;
159
+ default:
160
+ console.warn('Unhandled node click', object);
161
+ break;
162
+ }
163
+ };
164
+ return { onClickNode };
165
+ };
166
+
167
+ const useClickNotification = () => {
168
+ const { setDrawerType, setDrawerEntityId } = useDrawerStore();
169
+ const { markAsDismissed, markAsSeen } = useNotificationStore();
170
+ const onClickNotification = (notif, options) => {
171
+ const { id, crdType, target } = notif;
172
+ const { dismissToast } = options || {};
173
+ if (crdType && target) {
174
+ switch (crdType) {
175
+ case EntityTypes.InstrumentationRule:
176
+ setDrawerType(EntityTypes.InstrumentationRule);
177
+ setDrawerEntityId(getIdFromSseTarget(target, EntityTypes.InstrumentationRule));
178
+ break;
179
+ case EntityTypes.Source:
180
+ case 'InstrumentationConfig':
181
+ case 'InstrumentationInstance':
182
+ setDrawerType(EntityTypes.Source);
183
+ setDrawerEntityId(getIdFromSseTarget(target, EntityTypes.Source));
184
+ break;
185
+ case EntityTypes.Action:
186
+ setDrawerType(EntityTypes.Action);
187
+ setDrawerEntityId(getIdFromSseTarget(target, EntityTypes.Action));
188
+ break;
189
+ case EntityTypes.Destination:
190
+ case 'Destination':
191
+ setDrawerType(EntityTypes.Destination);
192
+ setDrawerEntityId(getIdFromSseTarget(target, EntityTypes.Destination));
193
+ break;
194
+ default:
195
+ console.warn('notif click not handled for:', { crdType, target });
196
+ break;
197
+ }
198
+ }
199
+ markAsSeen(id);
200
+ if (dismissToast)
201
+ markAsDismissed(id);
202
+ };
203
+ return { onClickNotification };
204
+ };
205
+
206
+ const INITIAL$1 = {
207
+ // @ts-ignore form should be initialized with empty values
208
+ type: '',
209
+ name: '',
210
+ exportedSignals: {
211
+ logs: false,
212
+ metrics: false,
213
+ traces: false,
214
+ },
215
+ fields: [],
216
+ };
217
+ const buildFormDynamicFields = (fields) => {
218
+ return fields
219
+ .filter((f) => !!f)
220
+ .map((f) => {
221
+ const { name, componentType, componentProperties, displayName, initialValue, renderCondition } = f;
222
+ switch (componentType) {
223
+ case FieldTypes.Dropdown: {
224
+ const componentPropertiesJson = safeJsonParse(componentProperties, {});
225
+ const options = Array.isArray(componentPropertiesJson.values)
226
+ ? componentPropertiesJson.values.map((value) => ({
227
+ id: value,
228
+ value,
229
+ }))
230
+ : Object.entries(componentPropertiesJson.values).map(([key, value]) => ({
231
+ id: key,
232
+ value,
233
+ }));
234
+ return {
235
+ name,
236
+ componentType: componentType,
237
+ title: displayName,
238
+ value: initialValue,
239
+ placeholder: componentPropertiesJson.placeholder || 'Select an option',
240
+ options,
241
+ renderCondition,
242
+ ...componentPropertiesJson,
243
+ };
244
+ }
245
+ default: {
246
+ const componentPropertiesJson = safeJsonParse(componentProperties, {});
247
+ return {
248
+ name,
249
+ componentType,
250
+ title: displayName,
251
+ value: initialValue,
252
+ renderCondition,
253
+ ...componentPropertiesJson,
254
+ };
255
+ }
256
+ }
257
+ });
258
+ };
259
+ const useDestinationFormData = (params) => {
260
+ const { supportedSignals, preLoadedFields } = params || {};
261
+ const { addNotification } = useNotificationStore();
262
+ const { formData, formErrors, handleFormChange, handleErrorChange, resetFormData } = useGenericForm(INITIAL$1);
263
+ const [yamlFields, setYamlFields] = useState([]);
264
+ const [dynamicFields, setDynamicFields] = useState([]);
265
+ useEffect(() => {
266
+ if (yamlFields) {
267
+ setDynamicFields(buildFormDynamicFields(yamlFields).map((field) => {
268
+ // if we have preloaded fields, we need to set the value of the field
269
+ // (this can be from an odigos-detected-destination during create, or from an existing destination during edit/update)
270
+ if (!!preLoadedFields) {
271
+ const parsedFields = typeof preLoadedFields === 'string' ? safeJsonParse(preLoadedFields, {}) : preLoadedFields;
272
+ if (field.name in parsedFields) {
273
+ return {
274
+ ...field,
275
+ // @ts-ignore
276
+ value: parsedFields[field.name],
277
+ };
278
+ }
279
+ }
280
+ return field;
281
+ }));
282
+ }
283
+ else {
284
+ setDynamicFields([]);
285
+ }
286
+ }, [yamlFields, preLoadedFields]);
287
+ useEffect(() => {
288
+ handleFormChange('fields', dynamicFields.map((field) => ({
289
+ key: field.name,
290
+ value: field.value,
291
+ })));
292
+ }, [dynamicFields]);
293
+ useEffect(() => {
294
+ const { logs, metrics, traces } = supportedSignals || {};
295
+ handleFormChange('exportedSignals', {
296
+ logs: logs?.supported || false,
297
+ metrics: metrics?.supported || false,
298
+ traces: traces?.supported || false,
299
+ });
300
+ }, [supportedSignals]);
301
+ const validateForm = (params) => {
302
+ const errors = {};
303
+ let ok = true;
304
+ dynamicFields.forEach(({ name, value, required }) => {
305
+ if (required && !value) {
306
+ ok = false;
307
+ errors[name] = FORM_ALERTS.FIELD_IS_REQUIRED;
308
+ }
309
+ });
310
+ if (!ok && params?.withAlert) {
311
+ addNotification({
312
+ type: StatusType.Warning,
313
+ title: params.alertTitle,
314
+ message: FORM_ALERTS.REQUIRED_FIELDS,
315
+ hideFromHistory: true,
316
+ });
317
+ }
318
+ handleErrorChange(undefined, undefined, errors);
319
+ return ok;
320
+ };
321
+ const loadFormWithDrawerItem = ({ destinationType: { type }, name, exportedSignals, fields }) => {
322
+ const updatedData = {
323
+ ...INITIAL$1,
324
+ type,
325
+ name,
326
+ exportedSignals,
327
+ fields: Object.entries(safeJsonParse(fields, {})).map(([key, value]) => ({ key, value })),
328
+ };
329
+ handleFormChange(undefined, undefined, updatedData);
330
+ };
331
+ return {
332
+ formData,
333
+ formErrors,
334
+ handleFormChange,
335
+ resetFormData,
336
+ validateForm,
337
+ loadFormWithDrawerItem,
338
+ yamlFields,
339
+ setYamlFields,
340
+ dynamicFields,
341
+ setDynamicFields,
342
+ };
343
+ };
344
+
345
+ const INITIAL = {
346
+ otelServiceName: '',
347
+ };
348
+ const useSourceFormData = () => {
349
+ const { addNotification } = useNotificationStore();
350
+ const { formData, formErrors, handleFormChange, handleErrorChange, resetFormData } = useGenericForm(INITIAL);
351
+ const validateForm = (params) => {
352
+ const errors = {};
353
+ let ok = true;
354
+ handleErrorChange(undefined, undefined, errors);
355
+ return ok;
356
+ };
357
+ const loadFormWithDrawerItem = ({ otelServiceName, name }) => {
358
+ const updatedData = {
359
+ ...INITIAL,
360
+ otelServiceName: otelServiceName || name || '',
361
+ };
362
+ handleFormChange(undefined, undefined, updatedData);
363
+ };
364
+ return {
365
+ formData,
366
+ formErrors,
367
+ handleFormChange,
368
+ resetFormData,
369
+ validateForm,
370
+ loadFormWithDrawerItem,
371
+ };
372
+ };
373
+
374
+ const useSourceSelectionFormData = (params) => {
375
+ const { namespaces } = useEntityStore();
376
+ const { selectedNamespace, onSelectNamespace, namespace } = params || {};
377
+ // only for "onboarding" - get unsaved values and set to state
378
+ // (this is to persist the values when user navigates back to this page)
379
+ const { configuredSources, configuredFutureApps, availableSources } = useSetupStore();
380
+ // Keeps intial values fetched from API, so we can later filter the user-specific-selections, therebey minimizing the amount of data sent to the API on "persist sources".
381
+ const [recordedInitialSources, setRecordedInitialSources] = useState(availableSources);
382
+ const [selectAllForNamespace, setSelectAllForNamespace] = useState('');
383
+ const [selectedSources, setSelectedSources] = useState(configuredSources);
384
+ const [selectedFutureApps, setSelectedFutureApps] = useState(configuredFutureApps);
385
+ useEffect(() => {
386
+ if (!!namespaces?.length) {
387
+ // initialize all states (to avoid undefined errors)
388
+ setRecordedInitialSources((prev) => {
389
+ const payload = { ...prev };
390
+ namespaces.forEach(({ name }) => (payload[name] = payload[name] || []));
391
+ return payload;
392
+ });
393
+ setSelectedSources((prev) => {
394
+ const payload = { ...prev };
395
+ namespaces.forEach(({ name }) => (payload[name] = payload[name] || []));
396
+ return payload;
397
+ });
398
+ setSelectedFutureApps((prev) => {
399
+ const payload = { ...prev };
400
+ namespaces.forEach(({ name, selected }) => (payload[name] = payload[name] || selected || false));
401
+ return payload;
402
+ });
403
+ }
404
+ }, [namespaces]);
405
+ useEffect(() => {
406
+ if (!!namespace) {
407
+ // initialize sources for this namespace
408
+ const { name, sources = [] } = namespace;
409
+ setRecordedInitialSources((prev) => ({
410
+ ...prev,
411
+ [name]: sources.map(({ name, kind, selected, numberOfInstances }) => ({
412
+ name,
413
+ kind,
414
+ selected,
415
+ numberOfInstances,
416
+ })),
417
+ }));
418
+ setSelectedSources((prev) => ({
419
+ ...prev,
420
+ [name]: !!prev[name].length
421
+ ? prev[name]
422
+ : sources.map(({ name, kind, selected, numberOfInstances }) => ({
423
+ name,
424
+ kind,
425
+ selected,
426
+ numberOfInstances,
427
+ })),
428
+ }));
429
+ }
430
+ }, [namespace]);
431
+ // form filters
432
+ const [searchText, setSearchText] = useState('');
433
+ const [showSelectedOnly, setShowSelectedOnly] = useState(false);
434
+ const onSelectAll = useCallback((selected, ns, selectionsByNamespace) => {
435
+ // When clicking "select all" on a single namespace
436
+ if (!!ns) {
437
+ if (!selectionsByNamespace) {
438
+ // If the sources are not loaded yet, call the onSelectNamespace to load the sources
439
+ onSelectNamespace?.(selected ? ns : '');
440
+ // Set the state, so the interval would be able to use the namespace
441
+ setSelectAllForNamespace(selected ? ns : '');
442
+ }
443
+ else if (!!selectionsByNamespace?.[ns]?.length) {
444
+ // Clear the state, so the interval would stop
445
+ setSelectAllForNamespace('');
446
+ }
447
+ // Set the selected sources
448
+ setSelectedSources((prev) => ({
449
+ ...prev,
450
+ [ns]: selectionsByNamespace?.[ns]?.map((source) => ({
451
+ ...source,
452
+ selected,
453
+ })) || [],
454
+ }));
455
+ // setSelectedFutureApps((prev) => ({
456
+ // ...prev,
457
+ // [ns]: !!selectionsByNamespace?.[ns]?.length ? selected : false,
458
+ // }))
459
+ }
460
+ // When clicking "select all" on all namespaces
461
+ else {
462
+ setSelectedSources((prev) => {
463
+ const payload = { ...prev };
464
+ Object.entries(payload).forEach(([key, sources]) => {
465
+ payload[key] = sources.map((source) => ({ ...source, selected }));
466
+ });
467
+ return payload;
468
+ });
469
+ }
470
+ }, [selectedSources]);
471
+ // This is to keep trying "select all" per namespace, until the sources are loaded (allows for 1-click, better UX).
472
+ useEffect(() => {
473
+ if (!!selectAllForNamespace) {
474
+ const interval = setInterval(() => onSelectAll(true, selectAllForNamespace, selectedSources), 100);
475
+ return () => clearInterval(interval);
476
+ }
477
+ }, [selectAllForNamespace, onSelectAll]);
478
+ const onSelectSource = (source, namespace) => {
479
+ const id = namespace || selectedNamespace;
480
+ if (!id)
481
+ return;
482
+ const arr = [...(selectedSources[id] || [])];
483
+ const foundIdx = arr.findIndex(({ name, kind }) => name === source.name && kind === source.kind);
484
+ if (foundIdx !== -1) {
485
+ // Replace the item with a new object to avoid mutating a possibly read-only object
486
+ const updatedItem = { ...arr[foundIdx], selected: !arr[foundIdx].selected };
487
+ arr[foundIdx] = updatedItem;
488
+ }
489
+ else {
490
+ arr.push({ ...source, selected: true });
491
+ }
492
+ setSelectedSources((prev) => ({ ...prev, [id]: arr }));
493
+ };
494
+ const onSelectFutureApps = (bool, namespace) => {
495
+ const id = namespace || selectedNamespace;
496
+ if (!id)
497
+ return;
498
+ setSelectedFutureApps((prev) => ({ ...prev, [id]: bool }));
499
+ };
500
+ const filterNamespaces = (options) => {
501
+ const { cancelSearch } = options || {};
502
+ const namespaces = Object.entries(selectedSources);
503
+ const isSearchOk = (targetText) => cancelSearch || !searchText || targetText.toLowerCase().includes(searchText);
504
+ return namespaces.filter(([namespace]) => isSearchOk(namespace));
505
+ };
506
+ const filterSources = (namespace, options) => {
507
+ const { cancelSearch, cancelSelected } = options || {};
508
+ const id = namespace || selectedNamespace;
509
+ if (!id)
510
+ return [];
511
+ const isSearchOk = (targetText) => cancelSearch || !searchText || targetText.toLowerCase().includes(searchText);
512
+ const isOnlySelectedOk = (sources, compareKey, target) => cancelSelected || !showSelectedOnly || !!sources.find((item) => item[compareKey] === target && item.selected);
513
+ return selectedSources[id].filter((source) => isSearchOk(source.name) && isOnlySelectedOk(selectedSources[id], 'name', source.name));
514
+ };
515
+ // This is to filter the user-specific-selections, therebey minimizing the amount of data sent to the API on "persist sources".
516
+ const getApiSourcesPayload = () => {
517
+ const payload = {};
518
+ Object.entries(selectedSources).forEach(([namespace, sources]) => {
519
+ sources.forEach((source) => {
520
+ const foundInitial = recordedInitialSources[namespace]?.find((initialSource) => initialSource.name === source.name && initialSource.kind === source.kind);
521
+ if (foundInitial?.selected !== source.selected) {
522
+ if (!payload[namespace])
523
+ payload[namespace] = [];
524
+ payload[namespace].push({
525
+ name: source.name,
526
+ kind: source.kind,
527
+ selected: source.selected,
528
+ });
529
+ }
530
+ });
531
+ });
532
+ return payload;
533
+ };
534
+ // This is to filter the user-specific-selections, therebey minimizing the amount of data sent to the API on "persist namespaces".
535
+ const getApiFutureAppsPayload = () => {
536
+ const payload = {};
537
+ Object.entries(selectedFutureApps).forEach(([namespace, selected]) => {
538
+ const foundInitial = namespaces?.find((ns) => ns.name === namespace);
539
+ if (foundInitial?.selected !== selected) {
540
+ payload[namespace] = selected;
541
+ }
542
+ });
543
+ return payload;
544
+ };
545
+ return {
546
+ recordedInitialSources,
547
+ filterNamespaces,
548
+ filterSources,
549
+ getApiSourcesPayload,
550
+ getApiFutureAppsPayload,
551
+ selectedSources,
552
+ onSelectSource,
553
+ selectedFutureApps,
554
+ onSelectFutureApps,
555
+ onSelectAll,
556
+ searchText,
557
+ setSearchText,
558
+ showSelectedOnly,
559
+ setShowSelectedOnly,
560
+ };
561
+ };
562
+
563
+ export { useClickNode as a, useClickNotification as b, useDestinationFormData as c, useSourceFormData as d, useSourceSelectionFormData as e, useActionFormData as u };