diff --git a/.changeset/metal-bottles-sin.md b/.changeset/metal-bottles-sin.md
new file mode 100644
index 0000000..36da3a9
--- /dev/null
+++ b/.changeset/metal-bottles-sin.md
@@ -0,0 +1,5 @@
+---
+'grafana-zabbix': patch
+---
+
+Fix: Remove props mutation in config editor
diff --git a/src/datasource/components/ConfigEditor.test.tsx b/src/datasource/components/ConfigEditor.test.tsx
new file mode 100644
index 0000000..e9ac9a4
--- /dev/null
+++ b/src/datasource/components/ConfigEditor.test.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import { ConfigEditor, Props } from './ConfigEditor';
+
+jest.mock('@grafana/runtime', () => ({
+ ...jest.requireActual('@grafana/runtime'),
+ config: {},
+}));
+
+describe('ConfigEditor', () => {
+ describe('on initial render', () => {
+ it('should not mutate the options object', () => {
+ const options = Object.freeze({ ...getDefaultOptions() }); // freezing the options to prevent mutations
+ Object.freeze(options.jsonData);
+ Object.freeze(options.secureJsonData);
+ Object.freeze(options.secureJsonFields);
+ const onOptionsChangeSpy = jest.fn();
+
+ expect(() => render()).not.toThrow();
+ });
+
+ it('should call onOptionsChange with the correct values', () => {
+ const options = Object.freeze({ ...getDefaultOptions() }); // freezing the options to prevent mutations
+ Object.freeze(options.jsonData);
+ Object.freeze(options.secureJsonData);
+ Object.freeze(options.secureJsonFields);
+ const onOptionsChangeSpy = jest.fn();
+
+ expect(() => render()).not.toThrow();
+ expect(onOptionsChangeSpy).toBeCalledTimes(1);
+ expect(onOptionsChangeSpy).toHaveBeenCalledWith({
+ ...getDefaultOptions(),
+ jsonData: {
+ ...getDefaultOptions().jsonData,
+ authType: 'userLogin',
+ timeout: undefined,
+ password: undefined, // password should be missing from jsonData
+ },
+ secureJsonData: {
+ ...getDefaultOptions().secureJsonData,
+ password: 'a password', // password should be present in secureJsonData
+ },
+ });
+ });
+
+ it('should not update password in secureJsonData if the field already exists in secureJsonFields', () => {
+ const options = Object.freeze({ ...getDefaultOptions(), secureJsonFields: { password: true } }); // freezing the options to prevent mutations
+ Object.freeze(options.jsonData);
+ Object.freeze(options.secureJsonData);
+ Object.freeze(options.secureJsonFields);
+ const onOptionsChangeSpy = jest.fn();
+
+ expect(() => render()).not.toThrow();
+ expect(onOptionsChangeSpy).toBeCalledTimes(1);
+ expect(onOptionsChangeSpy).toHaveBeenCalledWith({
+ ...getDefaultOptions(),
+ jsonData: {
+ ...getDefaultOptions().jsonData,
+ authType: 'userLogin',
+ timeout: undefined,
+ password: undefined, // password should be missing from jsonData
+ },
+ secureJsonData: {}, // password should be missing from secureJsonData
+ secureJsonFields: { ...getDefaultOptions().secureJsonFields, password: true },
+ });
+ });
+ });
+});
+
+function getDefaultOptions(): Props['options'] {
+ return {
+ id: 1,
+ orgId: 1,
+ uid: '',
+ name: '',
+ typeLogoUrl: '',
+ type: '',
+ typeName: '',
+ access: '',
+ url: '',
+ user: '',
+ database: '',
+ basicAuth: false,
+ basicAuthUser: '',
+ isDefault: false,
+ jsonData: {
+ cacheTTL: '',
+ dbConnectionEnable: false,
+ disableDataAlignment: false,
+ disableReadOnlyUsersAck: false,
+ trends: false,
+ trendsFrom: '',
+ trendsRange: '',
+ username: '',
+ password: 'a password',
+ },
+ readOnly: false,
+ secureJsonData: {},
+ secureJsonFields: {},
+ withCredentials: false,
+ };
+}
diff --git a/src/datasource/components/ConfigEditor.tsx b/src/datasource/components/ConfigEditor.tsx
index 2d79810..200ee3a 100644
--- a/src/datasource/components/ConfigEditor.tsx
+++ b/src/datasource/components/ConfigEditor.tsx
@@ -50,11 +50,11 @@ export const ConfigEditor = (props: Props) => {
// Set secureJsonFields.password to password and then remove it from config
const { password, ...restJsonData } = jsonData;
+
+ // Create new secureJsonData object
+ const newSecureJsonData = { ...options.secureJsonData };
if (!secureJsonFields?.password) {
- if (!options.secureJsonData) {
- options.secureJsonData = {};
- }
- options.secureJsonData.password = password;
+ newSecureJsonData.password = password;
}
onOptionsChange({
@@ -69,6 +69,7 @@ export const ConfigEditor = (props: Props) => {
disableDataAlignment: false,
...restJsonData,
},
+ secureJsonData: { ...newSecureJsonData },
});
if (options.jsonData.dbConnectionEnable) {
diff --git a/src/test-setup/jest-setup.js b/src/test-setup/jest-setup.js
index 1064ede..a8a964f 100644
--- a/src/test-setup/jest-setup.js
+++ b/src/test-setup/jest-setup.js
@@ -1,4 +1,7 @@
import { PanelCtrl, MetricsPanelCtrl } from './panelStub';
+import { TextEncoder, TextDecoder } from 'util';
+
+Object.assign(global, { TextDecoder, TextEncoder });
jest.mock(
'grafana/app/features/templating/template_srv',