Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve reusability of widget edit components #20843

Merged
merged 11 commits into from
Dec 3, 2024
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
Loading