Repository: splunk/addonfactory-ucc-generator (language: python)

Issue:
refactor: remove axios dependency
**Issue number:** ADDON-75748

## Summary

### Changes

- use native fetch for requests
- add generic methods for all method (get, post, delete)
- ResponseError is needed for distinguishing if the error was thrown by network or by JS itself

### User experience

No changes

## Checklist

If your change doesn't seem to apply, please leave them unchecked.

* [x] I have performed a self-review of this change
* [x] Changes have been tested
* [ ] Changes are documented
* [x] PR title follows [conventional commit semantics](https://www.conventionalcommits.org/en/v1.0.0/)



Discussion / hints:
it is good to know

<img width="1132" alt="image" src="https://github.com/user-attachments/assets/8fefd659-92d8-4376-8e22-3f3c54f3f737">

> Jest: "global" coverage threshold for branches (57%) not met: 56.99%

😪

Fix patch:
diff --git a/ui/src/util/api.ts b/ui/src/util/api.ts
index e44a7657e..b0265eaa9 100755
--- a/ui/src/util/api.ts
+++ b/ui/src/util/api.ts
@@ -5,9 +5,11 @@ import { generateToast, getUnifiedConfigs } from './util';
 import { parseErrorMsg } from './messageUtil';
 import { ResponseError } from './ResponseError';
 
+type ParamsRecord = Record<string, string | number | undefined>;
+
 export interface RequestParams {
     endpointUrl: string;
-    params?: Record<string, string | number>;
+    params?: ParamsRecord;
     signal?: AbortSignal;
     body?: BodyInit;
     handleError: boolean;
@@ -21,14 +23,14 @@ export function generateEndPointUrl(name: string) {
 
 const DEFAULT_PARAMS = { output_mode: 'json' };
 
-function createUrl(endpointUrl: string, params: Record<string, string | number>): URL {
+function createUrl(endpointUrl: string, params: ParamsRecord): URL {
     const url = new URL(
         createRESTURL(endpointUrl, { app, owner: 'nobody' }),
         window.location.origin
     );
-    Object.entries({ ...DEFAULT_PARAMS, ...params }).forEach(([key, value]) =>
-        url.searchParams.append(key, value.toString())
-    );
+    Object.entries({ ...DEFAULT_PARAMS, ...params })
+        .filter(([, value]) => value !== undefined && value !== null)
+        .forEach(([key, value]) => url.searchParams.append(key, value.toString()));
     return url;
 }
 


Test patch:
diff --git a/ui/src/components/SingleInputComponent/SingleInputComponent.test.tsx b/ui/src/components/SingleInputComponent/SingleInputComponent.test.tsx
index eec9726e6..f4c2b624d 100644
--- a/ui/src/components/SingleInputComponent/SingleInputComponent.test.tsx
+++ b/ui/src/components/SingleInputComponent/SingleInputComponent.test.tsx
@@ -49,8 +49,8 @@ const renderFeature = (additionalProps?: Partial<SingleInputComponentProps>) =>
 const mockedEntries = [
     { name: 'dataApiTest1', content: { testLabel: 'firstLabel', testValue: 'firstValue' } },
     { name: 'dataApiTest2', content: { testLabel: 'secondLabel', testValue: 'secondValue' } },
-    { name: 'dataApiTest3', content: { testLabel: 'thirdLabel', testValue: 'thirdValue' } },
-    { name: 'dataApiTest4', content: { testLabel: 'fourthLabel', testValue: 'fourthValue' } },
+    { name: 'true', content: { testLabel: 'thirdLabel', testValue: 'thirdValue' } },
+    { name: '0', content: { testLabel: 'fourthLabel', testValue: 'fourthValue' } },
 ];
 
 const MOCK_API_URL = '/demo_addon_for_splunk/some_API_endpint_for_select_data';
@@ -167,3 +167,67 @@ it.each([
     const { label } = autoCompleteFields[0];
     expect(within(inputComponent).getByText(label)).toBeInTheDocument();
 });
+
+it('should fetch options from API when endpointUrl is provided', async () => {
+    // server responses with a filtered mockedEntries based on the name parameter
+    server.use(
+        http.get(MOCK_API_URL, ({ request }) => {
+            const url = new URL(request.url);
+
+            const nameParameter = url.searchParams.get('name');
+            return HttpResponse.json(
+                getMockServerResponseForInput(
+                    mockedEntries.filter((entry) => entry.name === nameParameter)
+                )
+            );
+        })
+    );
+    const baseProps = {
+        ...defaultInputProps,
+        value: '',
+        controlOptions: {
+            createSearchChoice: true,
+            dependencies: ['name', 'region'],
+            endpointUrl: MOCK_API_URL,
+            labelField: 'testLabel',
+            valueField: 'testValue',
+        },
+    };
+    const { rerender } = render(<SingleInputComponent {...baseProps} />);
+
+    await userEvent.click(screen.getByRole('combobox'));
+    await screen.findByRole('menuitem', { name: 'No matches' });
+
+    // undefined value must be omitted
+    const firstEntry = mockedEntries[0];
+    rerender(
+        <SingleInputComponent
+            {...baseProps}
+            dependencyValues={{ name: firstEntry.name, region: undefined }}
+        />
+    );
+    await userEvent.click(screen.getByRole('combobox'));
+    await screen.findByRole('option', { name: firstEntry.content.testLabel });
+
+    const secondEntry = mockedEntries[1];
+    rerender(
+        <SingleInputComponent
+            {...baseProps}
+            dependencyValues={{ name: secondEntry.name, region: 1 }}
+        />
+    );
+    await userEvent.click(screen.getByRole('combobox'));
+    await screen.findByRole('option', { name: secondEntry.content.testLabel });
+
+    const thirdEntry = mockedEntries[2];
+    rerender(
+        <SingleInputComponent {...baseProps} dependencyValues={{ name: true, region: false }} />
+    );
+    await userEvent.click(screen.getByRole('combobox'));
+    await screen.findByRole('option', { name: thirdEntry.content.testLabel });
+
+    const fourthEntry = mockedEntries[3];
+    rerender(<SingleInputComponent {...baseProps} dependencyValues={{ name: 0, region: 0 }} />);
+    await userEvent.click(screen.getByRole('combobox'));
+    await screen.findByRole('option', { name: fourthEntry.content.testLabel });
+});