Skip to content

Commit

Permalink
feat(dashboard): add input widet configuration (#581)
Browse files Browse the repository at this point in the history
  • Loading branch information
NorbertNader authored Feb 16, 2023
1 parent 90a0790 commit a1bf180
Show file tree
Hide file tree
Showing 16 changed files with 413 additions and 14 deletions.
66 changes: 66 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/dashboard/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default {
},
],
},
setupFilesAfterEnv: ['mutationobserver-shim'],
setupFilesAfterEnv: ['mutationobserver-shim', '<rootDir>/testing/jest-setup.ts'],
//transform: {
// '.+\\.ts$': 'ts-jest',
// '^.+\\.tsx?$': 'ts-jest',
Expand Down
1 change: 1 addition & 0 deletions packages/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@swc/core": "^1.3.20",
"@swc/jest": "^0.2.23",
"@testing-library/dom": "^8.20.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@types/box-intersect": "^1.0.0",
Expand Down
7 changes: 6 additions & 1 deletion packages/dashboard/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ export default [
}),
commonjs(),
json(),
typescript({ tsconfig: './tsconfig.json' }),
typescript({
tsconfig: './tsconfig.json',
tsconfigOverride: {
exclude: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'],
},
}),
postcss({
plugins: [
url({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import React from 'react';

const InputComponent: React.FC = () => <div>input</div>;
const InputComponent: React.FC = () => {
return (
<svg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' className='palette-component-icon'>
<text x='8' y='15'>
I
</text>
<line x1='2.5' y1='20' x2='17.5' y2='20' stroke='inherit' strokeWidth='2'></line>
</svg>
);
};

export default InputComponent;
3 changes: 3 additions & 0 deletions packages/dashboard/src/components/sidePanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DashboardMessages } from '~/messages';
import { AppKitComponentTags } from '~/types';
import TextSettings from './sections/textSettingSection/text';
import LinkSettings from './sections/textSettingSection/link';
import InputSettings from './sections/inputSettingsSection';
import { BaseSettings } from './sections/baseSettingSection';
import AxisSetting from './sections/axisSettingSection';
import ThresholdsSection from './sections/thresholdsSection/thresholdsSection';
Expand All @@ -20,6 +21,7 @@ const SidePanel: FC<{ messageOverrides: DashboardMessages }> = ({ messageOverrid
const selectedWidget = selectedWidgets[0];
const isAppKitWidget = AppKitComponentTags.find((tag) => tag === selectedWidget.componentTag);
const isTextWidget = selectedWidget.componentTag === 'text';
const isInputWidget = selectedWidget.componentTag === 'input';

return (
<div className='iot-side-panel'>
Expand All @@ -28,6 +30,7 @@ const SidePanel: FC<{ messageOverrides: DashboardMessages }> = ({ messageOverrid
<BaseSettings messageOverrides={messageOverrides} />
{isTextWidget && <TextSettings messageOverride={messageOverrides} />}
{isTextWidget && <LinkSettings messageOverride={messageOverrides} />}
{isInputWidget && <InputSettings messageOverride={messageOverrides} />}
{isAppKitWidget && (
<>
<PropertiesAlarmsSection messageOverrides={messageOverrides} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React from 'react';
import { render } from '@testing-library/react';
import createWrapper from '@cloudscape-design/components/test-utils/dom';
import { Provider } from 'react-redux';
import InputSettings from './index';
import { InputWidget, RecursivePartial } from '~/types';
import { DashboardState } from '~/store/state';
import { DefaultDashboardMessages, DashboardMessages } from '~/messages';
import { configureDashboardStore } from '~/store';
import { MOCK_INPUT_WIDGET } from '../../../../../testing/mocks';
import { expect } from '@jest/globals';

const widget: InputWidget = { ...MOCK_INPUT_WIDGET };

const state: Partial<DashboardState> = {
dashboardConfiguration: {
widgets: [widget],
viewport: { duration: '5m' },
},
selectedWidgets: [widget],
};

const TestComponent = (
{
stateOverride = state,
messageOverride = DefaultDashboardMessages,
}: {
stateOverride?: RecursivePartial<DashboardState>;
messageOverride?: DashboardMessages;
} = { stateOverride: state, messageOverride: DefaultDashboardMessages }
) => (
<Provider store={configureDashboardStore(stateOverride)}>
<InputSettings messageOverride={messageOverride} />
</Provider>
);

it('renders without options from state', () => {
const props = { stateOverride: {} };
const { container } = render(<TestComponent {...props} />);
const inputSettings = createWrapper(container);
const options = inputSettings.findTokenGroup('[data-test-id="input-widget-token-list"]');

expect(options.findTokens().length).toBe(0);
});

it('renders with options from state', () => {
const { container } = render(<TestComponent />);
const inputSettings = createWrapper(container);
const options = inputSettings.findTokenGroup('[data-test-id="input-widget-token-list"]');

MOCK_INPUT_WIDGET.options.forEach(({ label }) => {
expect(options.getElement()).toHaveTextContent(label);
});
});

it('can add option', () => {
const { container } = render(<TestComponent />);
const inputSettings = createWrapper(container);
const addOptionButton = inputSettings.findButton('[data-test-id="input-widget-add-option-btn"]');
const optionInput = inputSettings.findInput('[data-test-id="input-widget-option-input"]');
const options = inputSettings.findTokenGroup('[data-test-id="input-widget-token-list"]');
const newOption = 'lorem ipsum';

expect(addOptionButton.isDisabled()).toBeTruthy();
expect(options.findTokens().length).toBe(3);
expect(options.getElement()).not.toHaveTextContent(newOption);

optionInput.setInputValue(newOption);

expect(addOptionButton.isDisabled()).toBeFalsy();
addOptionButton.click();

expect(options.findTokens().length).toBe(4);
expect(options.getElement()).toHaveTextContent(newOption);
});

it('can remove option', () => {
const { container } = render(<TestComponent />);
const inputSettings = createWrapper(container);
const options = inputSettings.findTokenGroup('[data-test-id="input-widget-token-list"]');

expect(options.findTokens().length).toBe(3);

options.findToken(2).findDismiss().click();

expect(options.findTokens().length).toBe(2);
expect(options.findToken(1).getElement()).toHaveTextContent(widget.options[0].label);
expect(options.findToken(2).getElement()).toHaveTextContent(widget.options[2].label);
});

it('correctly renders translations', () => {
const optionPlaceholder = 'lorem ipsum';
const addOptionLabel = 'lorem ipsum 2';
const props = {
messageOverride: {
...DefaultDashboardMessages,
sidePanel: {
...DefaultDashboardMessages.sidePanel,
inputSettings: {
...DefaultDashboardMessages.sidePanel.inputSettings,
optionPlaceholder,
addOptionLabel,
},
},
},
};
const { container } = render(<TestComponent {...props} />);
const inputSettings = createWrapper(container);
const addOptionButton = inputSettings.findButton('[data-test-id="input-widget-add-option-btn"]');
const optionInput = inputSettings.findInput('[data-test-id="input-widget-option-input"]');

expect(addOptionButton.getElement()).toHaveTextContent(addOptionLabel);
expect(optionInput.findNativeInput().getElement()).toHaveAttribute('placeholder', optionPlaceholder);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { FC, useState } from 'react';
import {
ExpandableSection,
Grid,
Input,
Button,
TokenGroup,
InputProps,
TokenGroupProps,
} from '@cloudscape-design/components';
import { NonCancelableEventHandler } from '@cloudscape-design/components/internal/events';
import { useInputWidgetInput } from '../../utils';
import { DashboardMessages } from '~/messages';

export type InputComponentProps = {
messageOverride: DashboardMessages;
};

const InputSettings: FC<InputComponentProps> = ({ messageOverride }) => {
const {
sidePanel: { inputSettings },
} = messageOverride;
const [options, setOptions] = useInputWidgetInput('options');
const [label, setLabel] = useState<string>();

const addOption: NonCancelableEventHandler<InputProps.ChangeDetail> = ({ detail: { value } }) => {
setLabel(value);
};

const saveOption = () => {
if (label) {
setOptions([...options, { label }]);
setLabel('');
}
};

const removeOption: NonCancelableEventHandler<TokenGroupProps.DismissDetail> = ({ detail: { itemIndex } }) => {
setOptions([...options.slice(0, itemIndex), ...options.slice(itemIndex + 1)]);
};

return (
<ExpandableSection headerText={inputSettings.title} defaultExpanded>
<Grid gridDefinition={[{ colspan: 9 }, { colspan: 3 }]}>
<Input
value={label || ''}
placeholder={inputSettings.optionPlaceholder}
onChange={addOption}
data-test-id='input-widget-option-input'
/>
<Button disabled={!label} onClick={saveOption} data-test-id='input-widget-add-option-btn'>
{inputSettings.addOptionLabel}
</Button>
</Grid>
<TokenGroup
data-test-id='input-widget-token-list'
alignment='vertical'
onDismiss={removeOption}
items={options}
/>
</ExpandableSection>
);
};

export default InputSettings;
4 changes: 2 additions & 2 deletions packages/dashboard/src/components/sidePanel/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Dispatch, useEffect, useState } from 'react';
// import { useDispatch } from 'react-redux';
import { cloneDeep, get, set } from 'lodash';
import { useDispatch, useSelector } from 'react-redux';
import { DashboardState } from '~/store/state';
import { onUpdateWidgetsAction } from '~/store/actions';
import { AppKitWidget, TextWidget, Widget } from '~/types';
import { AppKitWidget, TextWidget, InputWidget, Widget } from '~/types';

export type typedInputHook<T extends Widget = Widget> = <K extends keyof T>(key: K) => [T[K], Dispatch<T[K]>];

Expand Down Expand Up @@ -46,4 +45,5 @@ export const useInput: <T>(path: string, validator?: (newValue: T) => boolean) =

// this helps Typescript finding correct types given attribute names
export const useTextWidgetInput: typedInputHook<TextWidget> = useInput;
export const useInputWidgetInput: typedInputHook<InputWidget> = useInput;
export const useAppKitWidgetInput: typedInputHook<AppKitWidget> = useInput;
Loading

0 comments on commit a1bf180

Please sign in to comment.