Skip to content

Commit

Permalink
Improve reusability of widget edit components (#20843)
Browse files Browse the repository at this point in the history
* Remove redux store usage from `EditWidgetFrame`.

* Simplify `WidgetEditApplyAllChangesProvider`.

* Improve reusability of field select component.

* Decouple `EditWidgetFrame` from redux state.

* Simplify widget edit submit logic by removing `onSubmit` prop from widget edit components. Instead we only call `applyAllWidgetChanges` from `WidgetEditApplyAllChangesContext.

The `WidgetEditApplyAllChangesProvider` now calls a `onSubmit` prop which takes care of updateing the widget.

* Add note for `UPGRADING` doc.

* Allows readonly widget title.

* Unify names
  • Loading branch information
linuspahl authored Dec 3, 2024
1 parent 5c73faa commit d52fc07
Show file tree
Hide file tree
Showing 22 changed files with 317 additions and 253 deletions.
6 changes: 5 additions & 1 deletion UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ Upgrading to Graylog 6.2.x

## Breaking Changes

- tbd
### Plugins

Adjustment of `enterpriseWidgets` web interface plugin. The `editComponent` attribute now no longer has a `onSubmit` prop.
Before this change the prop had to be called to close the widget edit mode. Now it is enough to call `applyAllWidgetChanges` from the `WidgetEditApplyAllChangesContext`.
Alternatively the `SaveOrCancelButtons` component can be used in the edit component for custom widgets. It renders a cancel and submit button and calls `applyAllWidgetChanges` on submit.

## Configuration File Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ const StreamActions = ({
sendTelemetry(TELEMETRY_EVENT_TYPE.STREAMS.STREAM_ITEM_DATA_ROUTING_CLICKED, {
app_pathname: 'stream',
});
}}>Data Routing
}}>
Data routing
</Button>
</LinkContainer>
</IfPermitted>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ describe('AggregationWizard', () => {
<TestStoreProvider>
<FieldTypesContext.Provider value={fieldTypes}>
<AggregationWizard onChange={() => {}}
onSubmit={() => {}}
onCancel={() => {}}
config={widgetConfig}
editing
Expand Down Expand Up @@ -98,14 +97,6 @@ describe('AggregationWizard', () => {
await waitFor(() => expect(screen.queryByRole('menu')).not.toBeInTheDocument());
});

it('should call onSubmit', async () => {
const onSubmit = jest.fn();
renderSUT({ onSubmit });
userEvent.click(await screen.findByRole('button', { name: /update widget/i }));

await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
});

it('should call onCancel', async () => {
const onCancel = jest.fn();
renderSUT({ onCancel });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ const validateForm = (formValues: WidgetConfigFormValues) => {
return elementValidationResults.reduce((prev, cur) => ({ ...prev, ...cur }), {});
};

const AggregationWizard = ({ onChange, config, children, onSubmit, onCancel }: EditWidgetComponentProps<AggregationWidgetConfig> & { children: React.ReactElement }) => {
const AggregationWizard = ({ onChange, config, children, onCancel }: EditWidgetComponentProps<AggregationWidgetConfig> & { children: React.ReactElement }) => {
const initialFormValues = _initialFormValues(config);

return (
Expand All @@ -114,7 +114,6 @@ const AggregationWizard = ({ onChange, config, children, onSubmit, onCancel }: E
<ElementsConfiguration aggregationElementsByKey={aggregationElementsByKey}
config={config}
onCreate={onCreateElement}
onSubmit={onSubmit}
onCancel={onCancel}
onConfigChange={onChange} />
</Section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,18 @@ type Props = {
values: WidgetConfigFormValues,
setValues: (formValues: WidgetConfigFormValues) => void,
) => void,
onSubmit: () => void,
onCancel: () => void,
}

const ElementsConfiguration = ({ aggregationElementsByKey, config, onConfigChange, onCreate, onSubmit, onCancel }: Props) => {
const ElementsConfiguration = ({ aggregationElementsByKey, config, onConfigChange, onCreate, onCancel }: Props) => {
const { values, setValues } = useFormikContext<WidgetConfigFormValues>();

return (
<Container>
<StickyBottomActions actions={(
<>
<ElementsConfigurationActions />
<SaveOrCancelButtons onCancel={onCancel} onSubmit={onSubmit} />
<SaveOrCancelButtons onCancel={onCancel} />
</>
)}>
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,153 +15,23 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import type { SyntheticEvent } from 'react';
import { useCallback, useContext, useMemo } from 'react';
import * as Immutable from 'immutable';
import styled, { css } from 'styled-components';
import { useContext } from 'react';
import Immutable from 'immutable';

import { defaultCompare } from 'logic/DefaultCompare';
import FieldTypesContext from 'views/components/contexts/FieldTypesContext';
import Select from 'components/common/Select';
import type FieldTypeMapping from 'views/logic/fieldtypes/FieldTypeMapping';
import FieldTypeIcon from 'views/components/sidebar/fields/FieldTypeIcon';
import type FieldType from 'views/logic/fieldtypes/FieldType';
import FieldSelectBase from 'views/components/aggregationwizard/FieldSelectBase';
import useActiveQueryId from 'views/hooks/useActiveQueryId';
import type { SelectRef } from 'components/common/Select/Select';
import { Button } from 'components/bootstrap';

const FieldName = styled.span`
display: inline-flex;
gap: 2px;
align-items: center;
`;

const ButtonRow = styled.div`
margin-top: 5px;
display: inline-flex;
gap: 5px;
margin-bottom: 10px;
`;

type Props = {
ariaLabel?: string,
autoFocus?: boolean,
allowCreate?: boolean,
className?: string,
clearable?: boolean,
excludedFields?: Array<string>,
id: string,
isFieldQualified?: (field: FieldTypeMapping) => boolean,
menuPortalTarget?: HTMLElement,
name: string,
onChange: (fieldName: string) => void,
onMenuClose?: () => void,
openMenuOnFocus?: boolean,
persistSelection?: boolean,
placeholder?: string,
selectRef?: SelectRef,
size?: 'normal' | 'small',
value: string | undefined,
onSelectAllRest?: (fieldNames: Array<string>) => void,
showSelectAllRest?: boolean,
onDeSelectAll?: (e: SyntheticEvent) => void,
showDeSelectAll?: boolean,
}

const sortByLabel = ({ label: label1 }: { label: string }, { label: label2 }: { label: string }) => defaultCompare(label1, label2);

const UnqualifiedOption = styled.span(({ theme }) => css`
color: ${theme.colors.gray[70]};
`);

type OptionRendererProps = {
label: string,
qualified: boolean,
type?: FieldType,
};

const OptionRenderer = ({ label, qualified, type }: OptionRendererProps) => {
const children = (
<FieldName>
{type && <><FieldTypeIcon type={type} /> </>}{label}
</FieldName>
);
import FieldTypesContext from 'views/components/contexts/FieldTypesContext';

return qualified ? <span>{children}</span> : <UnqualifiedOption>{children}</UnqualifiedOption>;
};
type Props = Omit<React.ComponentProps<typeof FieldSelectBase>, 'options'>

const FieldSelect = ({
ariaLabel,
autoFocus,
allowCreate = false,
className,
clearable = false,
excludedFields = [],
id,
isFieldQualified = () => true,
menuPortalTarget,
name,
onChange,
onMenuClose,
openMenuOnFocus,
persistSelection,
placeholder,
selectRef,
size = 'small',
value,
onSelectAllRest,
showSelectAllRest = false,
onDeSelectAll,
showDeSelectAll = false,
}: Props) => {
const FieldSelect = (props: Props) => {
const activeQuery = useActiveQueryId();
const fieldTypes = useContext(FieldTypesContext);
const fieldOptions = useMemo(() => fieldTypes.queryFields
.get(activeQuery, Immutable.List())
.filter((field) => !excludedFields.includes(field.name))
.map((field) => ({
label: field.name,
value: field.name,
type: field.type,
qualified: isFieldQualified(field),
}))
.toArray()
.sort(sortByLabel), [activeQuery, excludedFields, fieldTypes.queryFields, isFieldQualified]);

const _onSelectAllRest = useCallback(() => onSelectAllRest(fieldOptions.map(({ value: fieldValue }) => fieldValue)), [fieldOptions, onSelectAllRest]);

const _showSelectAllRest = !!fieldOptions?.length && showSelectAllRest && typeof _onSelectAllRest === 'function';

const _showDeSelectAll = showDeSelectAll && typeof onDeSelectAll === 'function';
const fieldOptions = fieldTypes.queryFields.get(activeQuery, Immutable.List()).toArray();

return (
<>
<Select options={fieldOptions}
inputId={`select-${id}`}
forwardedRef={selectRef}
allowCreate={allowCreate}
className={className}
onMenuClose={onMenuClose}
openMenuOnFocus={openMenuOnFocus}
persistSelection={persistSelection}
clearable={clearable}
placeholder={placeholder}
name={name}
value={value}
aria-label={ariaLabel}
optionRenderer={OptionRenderer}
size={size}
autoFocus={autoFocus}
menuPortalTarget={menuPortalTarget}
onChange={onChange} />
{(_showSelectAllRest || _showDeSelectAll) && (
<ButtonRow>
{_showSelectAllRest && <Button bsSize="xs" onClick={_onSelectAllRest}>Select all fields</Button>}
{_showDeSelectAll && <Button bsSize="xs" onClick={onDeSelectAll}>Deselect all fields</Button>}
</ButtonRow>
)}
</>

<FieldSelectBase options={fieldOptions}
{...props} />
);
};

Expand Down
Loading

0 comments on commit d52fc07

Please sign in to comment.