From 878780f0682a784eec09885f45192d079b69115d Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Thu, 14 Sep 2023 12:28:28 +0200 Subject: [PATCH] feat: playground custom properties are nested (#4686) --- .../PlaygroundCodeFieldset.tsx | 15 +++- .../Playground/playground.utils.test.ts | 78 +++++++++++++++++++ .../playground/Playground/playground.utils.ts | 52 +++++++++++++ .../generateObjectCombinations.test.ts | 25 ++++++ .../playground/generateObjectCombinations.ts | 21 +++-- 5 files changed, 179 insertions(+), 12 deletions(-) create mode 100644 frontend/src/component/playground/Playground/playground.utils.test.ts diff --git a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundCodeFieldset.tsx b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundCodeFieldset.tsx index 78fd33c8a9..a0f93cc0c4 100644 --- a/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundCodeFieldset.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundForm/PlaygroundCodeFieldset/PlaygroundCodeFieldset.tsx @@ -27,7 +27,10 @@ import { formatUnknownError } from 'utils/formatUnknownError'; import useToast from 'hooks/useToast'; import { PlaygroundEditor } from './PlaygroundEditor/PlaygroundEditor'; import { parseDateValue, parseValidDate } from 'component/common/util'; -import { isStringOrStringArray } from '../../playground.utils'; +import { + isStringOrStringArray, + normalizeCustomContextProperties, +} from '../../playground.utils'; interface IPlaygroundCodeFieldsetProps { context: string | undefined; setContext: Dispatch>; @@ -58,7 +61,10 @@ export const PlaygroundCodeFieldset: VFC = ({ try { const contextValue = JSON.parse(input); - setFieldExist(contextValue[contextField] !== undefined); + setFieldExist( + contextValue[contextField] !== undefined || + contextValue?.properties[contextField] !== undefined + ); } catch (error: unknown) { return setError(formatUnknownError(error)); } @@ -75,12 +81,13 @@ export const PlaygroundCodeFieldset: VFC = ({ const onAddField = () => { try { const currentValue = JSON.parse(context || '{}'); + setContext( JSON.stringify( - { + normalizeCustomContextProperties({ ...currentValue, [contextField]: contextValue, - }, + }), null, 2 ) diff --git a/frontend/src/component/playground/Playground/playground.utils.test.ts b/frontend/src/component/playground/Playground/playground.utils.test.ts new file mode 100644 index 0000000000..16e53201e7 --- /dev/null +++ b/frontend/src/component/playground/Playground/playground.utils.test.ts @@ -0,0 +1,78 @@ +import { + normalizeCustomContextProperties, + NormalizedContextProperties, +} from './playground.utils'; + +test('should keep standard properties in their place', () => { + const input: NormalizedContextProperties = { + appName: 'testApp', + environment: 'testEnv', + userId: 'testUser', + sessionId: 'testSession', + remoteAddress: '127.0.0.1', + currentTime: 'now', + }; + const output = normalizeCustomContextProperties(input); + expect(output).toEqual(input); +}); + +test('should move non-standard properties to nested properties field', () => { + const input = { + appName: 'testApp', + customProp: 'customValue', + anotherCustom: 'anotherValue', + }; + const output = normalizeCustomContextProperties(input); + expect(output).toEqual({ + appName: 'testApp', + properties: { + customProp: 'customValue', + anotherCustom: 'anotherValue', + }, + }); +}); + +test('should not have properties field if there are no non-standard properties', () => { + const input = { + appName: 'testApp', + }; + const output = normalizeCustomContextProperties(input); + expect(output).toEqual(input); + expect(output.properties).toBeUndefined(); +}); + +test('should combine existing properties field with non-standard properties', () => { + const input = { + appName: 'testApp', + properties: { + existingProp: 'existingValue', + }, + customProp: 'customValue', + }; + const output = normalizeCustomContextProperties(input); + expect(output).toEqual({ + appName: 'testApp', + properties: { + existingProp: 'existingValue', + customProp: 'customValue', + }, + }); +}); + +test('should add multiple standard properties without breaking custom properties', () => { + const input = { + appName: 'testApp', + properties: { + existingProp: 'existingValue', + }, + currentTime: 'value', + }; + const output = normalizeCustomContextProperties(input); + expect(output).toEqual({ + appName: 'testApp', + currentTime: 'value', + properties: { + existingProp: 'existingValue', + }, + }); +}); diff --git a/frontend/src/component/playground/Playground/playground.utils.ts b/frontend/src/component/playground/Playground/playground.utils.ts index f3ae68cb97..9b68b94cb2 100644 --- a/frontend/src/component/playground/Playground/playground.utils.ts +++ b/frontend/src/component/playground/Playground/playground.utils.ts @@ -73,3 +73,55 @@ export const isStringOrStringArray = ( return false; }; + +type InputContextProperties = { + appName?: string; + environment?: string; + userId?: string; + sessionId?: string; + remoteAddress?: string; + currentTime?: string; + properties?: { [key: string]: any }; + [key: string]: any; +}; + +export type NormalizedContextProperties = Omit< + InputContextProperties, + 'properties' +> & { + properties?: { [key: string]: any }; +}; + +export const normalizeCustomContextProperties = ( + input: InputContextProperties +): NormalizedContextProperties => { + const standardProps = new Set([ + 'appName', + 'environment', + 'userId', + 'sessionId', + 'remoteAddress', + 'currentTime', + 'properties', + ]); + + const output: InputContextProperties = { ...input }; + let hasCustomProperties = false; + + for (const key in input) { + if (!standardProps.has(key)) { + if (!output.properties) { + output.properties = {}; + } + output.properties[key] = input[key]; + delete output[key]; + hasCustomProperties = true; + } + } + + if (!hasCustomProperties && !input.properties) { + delete output.properties; + } + + return output; +}; diff --git a/src/lib/features/playground/generateObjectCombinations.test.ts b/src/lib/features/playground/generateObjectCombinations.test.ts index e21356a594..e77dbfc1fc 100644 --- a/src/lib/features/playground/generateObjectCombinations.test.ts +++ b/src/lib/features/playground/generateObjectCombinations.test.ts @@ -37,3 +37,28 @@ test('should generate all combinations correctly when only one combination', () expect(actualCombinations).toEqual(expectedCombinations); }); + +test('should generate combinations with nested properties', () => { + const obj = { + sessionId: '1,2', + nonString: 2, + properties: { + nonString: 1, + channels: 'internet', + appName: 'a,b,c', + }, + }; + + const expectedCombinations = [ + { sessionId: '1', appName: 'a', channels: 'internet', nonString: 1 }, + { sessionId: '1', appName: 'b', channels: 'internet', nonString: 1 }, + { sessionId: '1', appName: 'c', channels: 'internet', nonString: 1 }, + { sessionId: '2', appName: 'a', channels: 'internet', nonString: 1 }, + { sessionId: '2', appName: 'b', channels: 'internet', nonString: 1 }, + { sessionId: '2', appName: 'c', channels: 'internet', nonString: 1 }, + ]; + + const actualCombinations = generateObjectCombinations(obj); + + expect(actualCombinations).toEqual(expectedCombinations); +}); diff --git a/src/lib/features/playground/generateObjectCombinations.ts b/src/lib/features/playground/generateObjectCombinations.ts index 78c1f073df..8f40ed270d 100644 --- a/src/lib/features/playground/generateObjectCombinations.ts +++ b/src/lib/features/playground/generateObjectCombinations.ts @@ -1,14 +1,19 @@ -type Dict = { [K in keyof T]: string[] }; +type Dict = { [K in keyof T]: (string | number)[] }; export const splitByComma = >( obj: T, -): Dict => - Object.fromEntries( - Object.entries(obj).map(([key, value]) => [ - key, - typeof value === 'string' ? value.split(',') : [value], - ]), - ) as Dict; +): Dict => { + return Object.entries(obj).reduce((acc, [key, value]) => { + if (key === 'properties' && typeof value === 'object') { + const nested = splitByComma(value as any); + return { ...acc, ...nested }; + } else if (typeof value === 'string') { + return { ...acc, [key]: value.split(',') }; + } else { + return { ...acc, [key]: [value] }; + } + }, {} as Dict); +}; export const generateCombinations = >( obj: Dict,