diff --git a/src/index.ts b/src/index.ts index 6b6f2a1..00b19ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export { ConnectionConfig, ConnectionConfigProps } from './ConnectionConfig'; +export { ConfigSelect, InlineInput } from './sql/ConfigEditor'; export { ResourceSelector, ResourceSelectorProps } from './sql/ResourceSelector'; export * from './types'; export * from './regions'; diff --git a/src/sql/ConfigEditor/ConfigSelect.test.tsx b/src/sql/ConfigEditor/ConfigSelect.test.tsx new file mode 100644 index 0000000..697590c --- /dev/null +++ b/src/sql/ConfigEditor/ConfigSelect.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { ConfigSelect, ConfigSelectProps } from './ConfigSelect'; +import { mockDatasourceOptions } from './__mocks__/datasource'; +import { select } from 'react-select-event'; + +const props: ConfigSelectProps = { + ...mockDatasourceOptions, + jsonDataPath: 'foo', + fetch: jest.fn(), + saveOptions: jest.fn(), +}; + +describe('SQLTextInput', () => { + it('should update jsonData', async () => { + const fetch = jest.fn().mockResolvedValue(['bar']); + const onOptionsChange = jest.fn(); + const label = 'foo-id'; + render(); + + const selectEl = screen.getByLabelText(label); + expect(selectEl).toBeInTheDocument(); + await select(selectEl, 'bar', { container: document.body }); + expect(fetch).toHaveBeenCalled(); + expect(onOptionsChange).toHaveBeenCalledWith({ + ...props.options, + jsonData: { + ...props.options.jsonData, + foo: 'bar', + }, + }); + }); + + it('should call deep nested jsonData value', async () => { + const onOptionsChange = jest.fn(); + const fetch = jest.fn().mockResolvedValue(['foobar']); + const label = 'foo-id'; + render( + + ); + const selectEl = screen.getByLabelText(label); + expect(selectEl).toBeInTheDocument(); + await select(selectEl, 'foobar', { container: document.body }); + expect(fetch).toHaveBeenCalled(); + expect(onOptionsChange).toHaveBeenCalledWith({ + ...props.options, + jsonData: { + ...props.options.jsonData, + foo: { + bar: 'foobar', + }, + }, + }); + }); +}); diff --git a/src/sql/ConfigEditor/ConfigSelect.tsx b/src/sql/ConfigEditor/ConfigSelect.tsx new file mode 100644 index 0000000..061699e --- /dev/null +++ b/src/sql/ConfigEditor/ConfigSelect.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { DataSourcePluginOptionsEditorProps, SelectableValue } from '@grafana/data'; +import { AwsAuthDataSourceJsonData, AwsAuthDataSourceSecureJsonData } from '../../types'; +import { ResourceSelector } from '../ResourceSelector'; +import { set, get } from 'lodash'; + +export interface ConfigSelectProps + extends DataSourcePluginOptionsEditorProps { + jsonDataPath: string; + fetch: () => Promise>>; + dependencies?: string[]; + label?: string; + 'data-testid'?: string; + hidden?: boolean; + disabled?: boolean; + jsonDataPathLabel?: string; + saveOptions: () => Promise; +} + +export function ConfigSelect(props: ConfigSelectProps) { + const { jsonData } = props.options; + const commonProps = { + title: jsonData.defaultRegion ? '' : 'select a default region', + disabled: !jsonData.defaultRegion, + labelWidth: 28, + className: 'width-30', + }; + const onChange = (e: SelectableValue | null) => { + const newOptions = { + ...props.options, + }; + set(newOptions.jsonData, props.jsonDataPath, e ? e.value || '' : e); + if (props.jsonDataPathLabel) { + set(newOptions.jsonData, props.jsonDataPathLabel, e ? e.label || '' : e); + } + props.onOptionsChange(newOptions); + }; + // Any change in the AWS connection details will affect selectors + const dependencies: string[] = [ + props.options.jsonData.assumeRoleArn, + props.options.jsonData.authType, + props.options.jsonData.defaultRegion, + props.options.jsonData.endpoint, + props.options.jsonData.externalId, + props.options.jsonData.profile, + ].concat(props.dependencies); + return ( + + ); +} diff --git a/src/sql/ConfigEditor/InlineInput.test.tsx b/src/sql/ConfigEditor/InlineInput.test.tsx new file mode 100644 index 0000000..28976f5 --- /dev/null +++ b/src/sql/ConfigEditor/InlineInput.test.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { InlineInputProps, InlineInput } from './InlineInput'; +import { mockDatasourceOptions } from './__mocks__/datasource'; + +const props: InlineInputProps = { + ...mockDatasourceOptions, + jsonDataPath: 'foo', +}; + +describe('InlineInput', () => { + it('should show jsonData value', () => { + render(); + expect(screen.queryByDisplayValue('bar')).toBeInTheDocument(); + }); + + it('should update jsonData', () => { + const testID = 'foo-id'; + const onOptionsChange = jest.fn(); + render(); + fireEvent.change(screen.getByTestId(testID), { target: { value: 'bar' } }); + expect(onOptionsChange).toHaveBeenCalledWith({ + ...props.options, + jsonData: { + ...props.options.jsonData, + foo: 'bar', + }, + }); + }); + + it('should update deep nested jsonData value', () => { + const onOptionsChange = jest.fn(); + const testID = 'foo-id'; + render(); + fireEvent.change(screen.getByTestId(testID), { target: { value: 'foobar' } }); + expect(onOptionsChange).toHaveBeenCalledWith({ + ...props.options, + jsonData: { + ...props.options.jsonData, + foo: { + bar: 'foobar', + }, + }, + }); + }); +}); diff --git a/src/sql/ConfigEditor/InlineInput.tsx b/src/sql/ConfigEditor/InlineInput.tsx new file mode 100644 index 0000000..89e18f3 --- /dev/null +++ b/src/sql/ConfigEditor/InlineInput.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; +import { AwsAuthDataSourceSecureJsonData } from '../../types'; +import { InlineField, Input } from '@grafana/ui'; +import { FormEvent } from 'react-dom/node_modules/@types/react'; +import { get, set } from 'lodash'; + +export interface InlineInputProps extends DataSourcePluginOptionsEditorProps<{}, AwsAuthDataSourceSecureJsonData> { + jsonDataPath: string; + label?: string; + tooltip?: string; + placeholder?: string; + 'data-testid'?: string; + hidden?: boolean; + disabled?: boolean; +} + +export function InlineInput(props: InlineInputProps) { + const onChange = (e: FormEvent) => { + const newOptions = { + ...props.options, + }; + set(newOptions.jsonData, props.jsonDataPath, e.currentTarget.value || ''); + props.onOptionsChange(newOptions); + }; + + return ( + + + + ); +} diff --git a/src/sql/ConfigEditor/__mocks__/datasource.ts b/src/sql/ConfigEditor/__mocks__/datasource.ts new file mode 100644 index 0000000..c1e1574 --- /dev/null +++ b/src/sql/ConfigEditor/__mocks__/datasource.ts @@ -0,0 +1,33 @@ +import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; +import { AwsAuthDataSourceJsonData, AwsAuthDataSourceSecureJsonData } from 'types'; + +export const mockDatasourceOptions: DataSourcePluginOptionsEditorProps< + AwsAuthDataSourceJsonData, + AwsAuthDataSourceSecureJsonData +> = { + options: { + id: 1, + uid: 'redshift-id', + orgId: 1, + name: 'Redshift', + typeLogoUrl: '', + type: '', + typeName: '', + access: '', + url: '', + password: '', + user: '', + basicAuth: false, + basicAuthPassword: '', + basicAuthUser: '', + database: '', + isDefault: false, + jsonData: { + defaultRegion: 'us-east-2', + }, + secureJsonFields: {}, + readOnly: false, + withCredentials: false, + }, + onOptionsChange: jest.fn(), +}; diff --git a/src/sql/ConfigEditor/index.ts b/src/sql/ConfigEditor/index.ts new file mode 100644 index 0000000..f2fd85c --- /dev/null +++ b/src/sql/ConfigEditor/index.ts @@ -0,0 +1,2 @@ +export { ConfigSelect, ConfigSelectProps } from './ConfigSelect'; +export { InlineInput, InlineInputProps } from './InlineInput'; diff --git a/src/sql/ResourceSelector.tsx b/src/sql/ResourceSelector.tsx index 05218ec..cd093f6 100644 --- a/src/sql/ResourceSelector.tsx +++ b/src/sql/ResourceSelector.tsx @@ -12,6 +12,7 @@ export type ResourceSelectorProps = { tooltip?: string; label?: string; 'data-testid'?: string; + hidden?: boolean; // Options only needed for QueryEditor default?: string; // Options only needed for the ConfigEditor @@ -99,7 +100,7 @@ export function ResourceSelector(props: ResourceSelectorProps) { }; return ( - + ; diff --git a/yarn.lock b/yarn.lock index dbace5e..b14d780 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3753,15 +3753,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001181: - version "1.0.30001204" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001204.tgz#256c85709a348ec4d175e847a3b515c66e79f2aa" - integrity sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ== - -caniuse-lite@^1.0.30001125: - version "1.0.30001276" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001276.tgz#7049685eb972eb09c0ecbb57227b489d76244fb1" - integrity sha512-psUNoaG1ilknZPxi8HuhQWobuhLqtYSRUxplfVkEJdgZNB9TETVYGSBtv4YyfAdGvE6gn2eb0ztiXqHoWJcGnw== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001181: + version "1.0.30001279" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001279.tgz" + integrity sha512-VfEHpzHEXj6/CxggTwSFoZBBYGQfQv9Cf42KPlO79sWXCD1QNKWKsKzFeWL7QpZHJQYAvocqV6Rty1yJMkqWLQ== capture-exit@^2.0.0: version "2.0.0"