Skip to content

Commit

Permalink
Migrate to new form styling in config and query editors (#289)
Browse files Browse the repository at this point in the history
  • Loading branch information
idastambuk authored Jul 1, 2024
1 parent 0c83069 commit bb1b2d8
Show file tree
Hide file tree
Showing 9 changed files with 410 additions and 923 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"devDependencies": {
"@babel/core": "^7.16.7",
"@emotion/css": "^11.1.3",
"@grafana/aws-sdk": "0.3.1",
"@grafana/aws-sdk": "0.4.0",
"@grafana/data": "10.2.0",
"@grafana/eslint-config": "^5.1.0",
"@grafana/runtime": "10.2.0",
Expand Down
84 changes: 29 additions & 55 deletions src/datasource/components/ConfigEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DataSourceSettings } from '@grafana/data';
import { TwinMakerDataSourceOptions, TwinMakerSecureJsonData } from 'datasource/types';
import { config } from '@grafana/runtime';

const datasourceOptions: DataSourceSettings<TwinMakerDataSourceOptions, TwinMakerSecureJsonData> = {
id: 0,
Expand All @@ -26,7 +25,6 @@ const datasourceOptions: DataSourceSettings<TwinMakerDataSourceOptions, TwinMake
withCredentials: false,
secureJsonFields: {},
};
const originalFormFeatureToggleValue = config.featureToggles.awsDatasourcesNewFormStyling;
const workspacesMock = jest.fn(() => Promise.resolve([{ value: 'test1', label: 'test1' }]));

jest.mock('common/datasourceSrv', () => ({
Expand All @@ -50,62 +48,38 @@ const resetWindow = () => {
settings: {},
};
};
const cleanup = () => {
config.featureToggles.awsDatasourcesNewFormStyling = originalFormFeatureToggleValue;
};

describe('ConfigEditor', () => {
beforeEach(() => resetWindow());
function run() {
it('should display an error if the datasource is not saved', async () => {
const { rerender, user } = setup();
const rerenderOptions = {
...datasourceOptions,
jsonData: {
assumeRoleArn: 'test_2',
},
};
rerender(<ConfigEditor options={rerenderOptions} onOptionsChange={() => {}} />);
const dropdown = screen.getByText('Select a workspace');
await user.click(dropdown);
const error = await screen.findByText('Save the datasource first to load workspaces');
expect(error).toBeInTheDocument();
expect(workspacesMock).not.toHaveBeenCalled();
});
it('should remove the error when the datasource is saved', async () => {
const { rerender, user } = setup();
const rerenderOptions = {
...datasourceOptions,
jsonData: {
assumeRoleArn: 'test_2',
},
};
rerender(<ConfigEditor options={rerenderOptions} onOptionsChange={() => {}} />);
const dropdown = screen.getByText('Select a workspace');
await user.click(dropdown);
const error = await screen.findByText('Save the datasource first to load workspaces');
expect(error).toBeInTheDocument();
rerender(<ConfigEditor options={{ ...rerenderOptions, version: 2 }} onOptionsChange={() => {}} />);
waitFor(() => expect(error).not.toBeInTheDocument());
});
}

describe('Loading Workspaces with awsDatasourcesNewFormStyling feature toggle disabled', () => {
beforeAll(() => {
config.featureToggles.awsDatasourcesNewFormStyling = false;
});
afterAll(() => {
cleanup();
});
run();
it('should display an error if the datasource is not saved', async () => {
const { rerender, user } = setup();
const rerenderOptions = {
...datasourceOptions,
jsonData: {
assumeRoleArn: 'test_2',
},
};
rerender(<ConfigEditor options={rerenderOptions} onOptionsChange={() => {}} />);
const dropdown = screen.getByText('Select a workspace');
await user.click(dropdown);
const error = await screen.findByText('Save the datasource first to load workspaces');
expect(error).toBeInTheDocument();
expect(workspacesMock).not.toHaveBeenCalled();
});
describe('Loading Workspaces with awsDatasourcesNewFormStyling feature toggle enabled', () => {
beforeAll(() => {
config.featureToggles.awsDatasourcesNewFormStyling = true;
});
afterAll(() => {
cleanup();
});
run();
it('should remove the error when the datasource is saved', async () => {
const { rerender, user } = setup();
const rerenderOptions = {
...datasourceOptions,
jsonData: {
assumeRoleArn: 'test_2',
},
};
rerender(<ConfigEditor options={rerenderOptions} onOptionsChange={() => {}} />);
const dropdown = screen.getByText('Select a workspace');
await user.click(dropdown);
const error = await screen.findByText('Save the datasource first to load workspaces');
expect(error).toBeInTheDocument();
rerender(<ConfigEditor options={{ ...rerenderOptions, version: 2 }} onOptionsChange={() => {}} />);
waitFor(() => expect(error).not.toBeInTheDocument());
});
});
146 changes: 45 additions & 101 deletions src/datasource/components/ConfigEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React, { useEffect, useState } from 'react';
import { onUpdateDatasourceJsonDataOption, SelectableValue, updateDatasourcePluginJsonDataOption } from '@grafana/data';
import { ConnectionConfig, ConnectionConfigProps, Divider } from '@grafana/aws-sdk';
import { FieldSet, InlineField, InlineFieldRow, Select, Input, Alert, Checkbox, Field, Switch } from '@grafana/ui';
import { Select, Input, Alert, Field, Switch } from '@grafana/ui';
import { standardRegions } from '../regions';
import { TwinMakerDataSourceOptions, TwinMakerSecureJsonData } from '../types';
import { getTwinMakerDatasource } from 'common/datasourceSrv';
import { getSelectionInfo } from 'common/info/info';
import { SelectableQueryResults } from 'common/info/types';
import { useEffectOnce } from 'react-use';
import { config } from '@grafana/runtime';
import { ConfigSection } from '@grafana/experimental';

type Props = ConnectionConfigProps<TwinMakerDataSourceOptions, TwinMakerSecureJsonData>;
Expand All @@ -21,8 +20,6 @@ export function ConfigEditor(props: Props) {
const [isLoadingWorkspaces, setLoadingWorkspaces] = useState(false);
const [saved, setSaved] = useState(!!props.options.version && props.options.version > 1);

const newFormStylingEnabled = config.featureToggles.awsDatasourcesNewFormStyling;

useEffectOnce(() => {
// Default to 'us-east-1'
if (!props.options.jsonData?.defaultRegion) {
Expand Down Expand Up @@ -92,7 +89,7 @@ export function ConfigEditor(props: Props) {

return (
<div className="width-30">
<ConnectionConfig {...props} standardRegions={standardRegions} newFormStylingEnabled={newFormStylingEnabled} />
<ConnectionConfig {...props} standardRegions={standardRegions} />
{!props.options.jsonData.assumeRoleArn && (
<Alert title="Assume Role ARN" severity="error" style={{ width: 700 }}>
Specify an IAM role to narrow the permission scope of this datasource. Follow the documentation{' '}
Expand All @@ -106,105 +103,52 @@ export function ConfigEditor(props: Props) {
to create policies and a role with minimal permissions for your TwinMaker workspace.
</Alert>
)}
{newFormStylingEnabled ? (
<>
<Divider />
<ConfigSection title="Twinmaker Settings" data-testid="twinmaker-settings">
<Field label="Workspace" invalid={!!workspacesError} error={workspacesError}>
<Select
menuPlacement="top"
menuShouldPortal={true}
value={workspacesSelection.current}
options={workspacesSelection.options}
className="width-30"
onChange={onWorkspaceChange}
isLoading={isLoadingWorkspaces}
allowCustomValue={true}
onCreateOption={onUnknownWorkspaceChange}
formatCreateLabel={(v) => `WorkspaceID: ${v}`}
isClearable={true}
placeholder="Select a workspace"
noOptionsMessage="No workspaces found"
onOpenMenu={onOpenHandler}
onCloseMenu={() => setIsWorkspacesMenuOpen(false)}
isOpen={isWorkspacesMenuOpen}
invalid={!!workspacesError}
/>
</Field>
<Field htmlFor="alarmConfigChecked" label="Define write permissions for Alarm Configuration Panel">
<Switch
id="alarmConfigChecked"
value={alarmConfigChecked}
onChange={(e) => onAlarmCheckChange(e.currentTarget.checked)}
/>
</Field>

{alarmConfigChecked && (
<Field
htmlFor="assumeRoleArnWriter"
label="Assume Role ARN Write"
description="Specify the ARN of a role to assume when writing property values in IoT TwinMaker"
>
<Input
id="assumeRoleArnWriter"
placeholder="arn:aws:iam:*"
value={props.options.jsonData.assumeRoleArnWriter || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'assumeRoleArnWriter')}
/>
</Field>
)}
</ConfigSection>
</>
) : (
<FieldSet label={'TwinMaker settings'} data-testid="twinmaker-settings">
<InlineFieldRow>
<InlineField label="Workspace" labelWidth={28} invalid={!!workspacesError} error={workspacesError}>
<Select
menuPlacement="top"
menuShouldPortal={true}
value={workspacesSelection.current}
options={workspacesSelection.options}
className="width-30"
onChange={onWorkspaceChange}
isLoading={isLoadingWorkspaces}
allowCustomValue={true}
onCreateOption={onUnknownWorkspaceChange}
formatCreateLabel={(v) => `WorkspaceID: ${v}`}
isClearable={true}
disabled={workspaces?.length === 0}
placeholder="Select a workspace"
noOptionsMessage="No workspaces found"
onOpenMenu={onOpenHandler}
onCloseMenu={() => setIsWorkspacesMenuOpen(false)}
isOpen={isWorkspacesMenuOpen}
invalid={!!workspacesError}
/>
</InlineField>
</InlineFieldRow>
<Checkbox
label={'Define write permissions for Alarm Configuration Panel'}
<Divider />
<ConfigSection title="Twinmaker Settings" data-testid="twinmaker-settings">
<Field label="Workspace" invalid={!!workspacesError} error={workspacesError}>
<Select
menuPlacement="top"
menuShouldPortal={true}
value={workspacesSelection.current}
options={workspacesSelection.options}
className="width-30"
onChange={onWorkspaceChange}
isLoading={isLoadingWorkspaces}
allowCustomValue={true}
onCreateOption={onUnknownWorkspaceChange}
formatCreateLabel={(v) => `WorkspaceID: ${v}`}
isClearable={true}
placeholder="Select a workspace"
noOptionsMessage="No workspaces found"
onOpenMenu={onOpenHandler}
onCloseMenu={() => setIsWorkspacesMenuOpen(false)}
isOpen={isWorkspacesMenuOpen}
invalid={!!workspacesError}
/>
</Field>
<Field htmlFor="alarmConfigChecked" label="Define write permissions for Alarm Configuration Panel">
<Switch
id="alarmConfigChecked"
value={alarmConfigChecked}
onChange={(e) => onAlarmCheckChange(e.currentTarget.checked)}
/>
{alarmConfigChecked && (
<InlineFieldRow>
<InlineField
label="Assume Role ARN Write"
labelWidth={28}
tooltip="Specify the ARN of a role to assume when writing property values in IoT TwinMaker"
>
<Input
aria-label="Assume Role ARN Write"
className="width-30"
placeholder="arn:aws:iam:*"
value={props.options.jsonData.assumeRoleArnWriter || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'assumeRoleArnWriter')}
/>
</InlineField>
</InlineFieldRow>
)}
</FieldSet>
)}
</Field>

{alarmConfigChecked && (
<Field
htmlFor="assumeRoleArnWriter"
label="Assume Role ARN Write"
description="Specify the ARN of a role to assume when writing property values in IoT TwinMaker"
>
<Input
id="assumeRoleArnWriter"
placeholder="arn:aws:iam:*"
value={props.options.jsonData.assumeRoleArnWriter || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'assumeRoleArnWriter')}
/>
</Field>
)}
</ConfigSection>
</div>
);
}
57 changes: 3 additions & 54 deletions src/datasource/components/FilterQueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { Button, InlineField, InlineFieldRow, Select } from '@grafana/ui';
import { Button, Select } from '@grafana/ui';
import { DEFAULT_PROPERTY_FILTER_OPERATOR, TwinMakerFilterValue, TwinMakerPropertyFilter } from 'common/manager';
import { editorFieldStyles, firstLabelWidth } from '.';
import { editorFieldStyles } from '.';
import { BlurTextInput } from './BlurTextInput';
import { SelectableValue } from '@grafana/data';
import { EditorField, EditorFieldGroup } from '@grafana/experimental';
Expand All @@ -13,7 +13,6 @@ export interface FilterQueryEditorProps {
onChange: (index: number, filter?: TwinMakerPropertyFilter, isTabularCondition?: boolean) => void;
onAdd: () => void;
isTabularCondition?: boolean;
newFormStylingEnabled?: boolean;
}

export default function FilterQueryEditor(props: FilterQueryEditorProps) {
Expand Down Expand Up @@ -63,7 +62,7 @@ export default function FilterQueryEditor(props: FilterQueryEditorProps) {
return '';
};

return props.newFormStylingEnabled ? (
return (
<EditorFieldGroup>
{filters.map((f, index) => (
<EditorField
Expand Down Expand Up @@ -130,56 +129,6 @@ export default function FilterQueryEditor(props: FilterQueryEditorProps) {
</EditorField>
))}
</EditorFieldGroup>
) : (
<InlineFieldRow>
{filters.map((f, index) => (
<InlineField
key={`${index}/${f.name}`}
label={'Filter'}
grow={true}
labelWidth={firstLabelWidth}
tooltip="Enter expressions to filter property values"
>
<>
<Select
menuShouldPortal={true}
options={properties}
value={properties.find((v) => v.value === f.name)}
onChange={(v) => onNameChange(v, index)}
placeholder="Select a property"
isClearable={false}
width={40}
/>
<BlurTextInput
width={14}
value={f.op ?? DEFAULT_PROPERTY_FILTER_OPERATOR}
onChange={(v) => {
if (v) {
onOpChange(v, index);
}
}}
placeholder={DEFAULT_PROPERTY_FILTER_OPERATOR}
/>
<BlurTextInput
value={filterValueToString(filters[index].value)}
onChange={(v) => {
if (v) {
onValueChange(v, index);
}
}}
placeholder="value"
width={40}
/>
<Button
icon="trash-alt"
variant="secondary"
onClick={() => onChange(index, undefined, isTabularCondition)} // Do not send event
/>
{index === filters.length - 1 && <Button icon="plus-circle" variant="secondary" onClick={props.onAdd} />}
</>
</InlineField>
))}
</InlineFieldRow>
);
}
function isFilterEmpty(filter: TwinMakerPropertyFilter) {
Expand Down
Loading

0 comments on commit bb1b2d8

Please sign in to comment.