Skip to content

Commit e67cad2

Browse files
committed
simple in-token grouping
1 parent ef43dee commit e67cad2

File tree

4 files changed

+150
-63
lines changed

4 files changed

+150
-63
lines changed

pages/property-filter/split-panel-app-layout-integration.page.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3-
import React from 'react';
3+
import React, { useState } from 'react';
44
import AppLayout from '~components/app-layout';
55
import SplitPanel from '~components/split-panel';
66
import Box from '~components/box';
77
import Table from '~components/table';
8-
import PropertyFilter from '~components/property-filter';
8+
import PropertyFilter, { PropertyFilterProps } from '~components/property-filter';
99
import Header from '~components/header';
1010
import Button from '~components/button';
1111
import ScreenshotArea from '../utils/screenshot-area';
@@ -41,6 +41,21 @@ export default function () {
4141
sorting: {},
4242
});
4343

44+
// TODO: add collection-hooks support for nested queries
45+
const [query, setQuery] = useState<PropertyFilterProps.Query>({
46+
tokens: [],
47+
tokenGroups: [
48+
{
49+
operation: 'and',
50+
tokens: [
51+
{ propertyKey: 'averagelatency', operator: '>=', value: '30' },
52+
{ propertyKey: 'averagelatency', operator: '<', value: '60' },
53+
],
54+
},
55+
],
56+
operation: 'or',
57+
});
58+
4459
const filteringOptions = propertyFilterProps.filteringOptions.map(option => {
4560
if (option.propertyKey === 'state') {
4661
option.label = states[parseInt(option.value)];
@@ -85,6 +100,8 @@ export default function () {
85100
filter={
86101
<PropertyFilter
87102
{...propertyFilterProps}
103+
query={query}
104+
onChange={event => setQuery(event.detail)}
88105
filteringOptions={filteringOptions}
89106
virtualScroll={true}
90107
countText={`${items.length} matches`}

src/property-filter/styles.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@
7373
// The below code cancels horizontal padding of the popover and horizontal margin of the token-editor.
7474
padding-inline-end: calc(#{awsui.$space-m} + #{awsui.$space-xxs});
7575
margin-inline: calc(-1 * #{awsui.$space-m} + -1 * #{awsui.$space-xxs});
76+
margin-top: awsui.$space-s;
77+
}
78+
&-form {
79+
border-radius: 4px;
80+
border: 2px solid awsui.$color-border-divider-default;
81+
padding: awsui.$space-xs;
7682
}
7783
}
7884
.property-editor {

src/property-filter/token-editor.tsx

Lines changed: 124 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { DropdownStatusProps } from '../internal/components/dropdown-status/inte
3131
import InternalButton from '../button/internal';
3232
import InternalFormField from '../form-field/internal';
3333
import { matchTokenValue } from './utils';
34+
import InternalBox from '../box/internal';
3435

3536
interface PropertyInputProps {
3637
asyncProps: null | DropdownStatusProps;
@@ -215,37 +216,59 @@ export function TokenEditor({
215216
token,
216217
triggerComponent,
217218
}: TokenEditorProps) {
218-
const firstToken = token.tokens[0] as InternalToken;
219-
const [temporaryToken, setTemporaryToken] = useState<InternalToken>(firstToken);
219+
const firstLevelTokens: InternalToken[] = [];
220+
for (const tokenOrGroup of token.tokens) {
221+
if ('operation' in tokenOrGroup) {
222+
// ignore as deeply nested tokens are not supported
223+
} else {
224+
firstLevelTokens.push(tokenOrGroup);
225+
}
226+
}
227+
const [operation, setOperation] = useState(token.operation);
228+
const [temp, setTemp] = useState<InternalToken[]>(firstLevelTokens);
220229
const popoverRef = useRef<InternalPopoverRef>(null);
221230
const closePopover = () => {
222231
popoverRef.current && popoverRef.current.dismissPopover();
223232
};
224233

225-
const property = temporaryToken.property;
226-
const onChangePropertyKey = (newPropertyKey: undefined | string) => {
227-
const filteringProperty = filteringProperties.reduce<InternalFilteringProperty | undefined>(
228-
(acc, property) => (property.propertyKey === newPropertyKey ? property : acc),
229-
undefined
230-
);
231-
const allowedOperators = filteringProperty ? getAllowedOperators(filteringProperty) : freeTextFiltering.operators;
232-
const operator =
233-
temporaryToken.operator && allowedOperators.indexOf(temporaryToken.operator) !== -1
234-
? temporaryToken.operator
235-
: allowedOperators[0];
236-
const matchedProperty = filteringProperties.find(property => property.propertyKey === newPropertyKey) ?? null;
237-
setTemporaryToken({ ...temporaryToken, property: matchedProperty, operator });
238-
};
234+
const groups = temp.map((temporaryToken, index) => {
235+
const setTemporaryToken = (newToken: InternalToken) => {
236+
setTemp(prev => {
237+
const copy = [...prev];
238+
copy[index] = newToken;
239+
return copy;
240+
});
241+
};
239242

240-
const operator = temporaryToken.operator;
241-
const onChangeOperator = (newOperator: ComparisonOperator) => {
242-
setTemporaryToken({ ...temporaryToken, operator: newOperator });
243-
};
243+
const property = temporaryToken.property;
244+
const onChangePropertyKey = (newPropertyKey: undefined | string) => {
245+
const filteringProperty = filteringProperties.reduce<InternalFilteringProperty | undefined>(
246+
(acc, property) => (property.propertyKey === newPropertyKey ? property : acc),
247+
undefined
248+
);
249+
const allowedOperators = filteringProperty ? getAllowedOperators(filteringProperty) : freeTextFiltering.operators;
250+
const operator =
251+
temporaryToken.operator && allowedOperators.indexOf(temporaryToken.operator) !== -1
252+
? temporaryToken.operator
253+
: allowedOperators[0];
254+
const matchedProperty = filteringProperties.find(property => property.propertyKey === newPropertyKey) ?? null;
255+
setTemporaryToken({ ...temporaryToken, property: matchedProperty, operator });
256+
};
244257

245-
const value = temporaryToken.value;
246-
const onChangeValue = (newValue: string) => {
247-
setTemporaryToken({ ...temporaryToken, value: newValue });
248-
};
258+
const operator = temporaryToken.operator;
259+
const onChangeOperator = (newOperator: ComparisonOperator) => {
260+
setTemporaryToken({ ...temporaryToken, operator: newOperator });
261+
};
262+
263+
const value = temporaryToken.value;
264+
const onChangeValue = (newValue: string) => {
265+
setTemporaryToken({ ...temporaryToken, value: newValue });
266+
};
267+
268+
return { property, onChangePropertyKey, operator, onChangeOperator, value, onChangeValue };
269+
});
270+
271+
const operationOptions = [{ value: 'and' }, { value: 'or' }];
249272

250273
return (
251274
<InternalPopover
@@ -256,47 +279,86 @@ export function TokenEditor({
256279
size="large"
257280
position="right"
258281
dismissAriaLabel={i18nStrings.dismissAriaLabel}
259-
__onOpen={() => setTemporaryToken(firstToken)}
282+
__onOpen={() => setTemp(firstLevelTokens)}
260283
renderWithPortal={expandToViewport}
261284
content={
262285
<div className={styles['token-editor']}>
263-
<div className={styles['token-editor-form']}>
264-
<InternalFormField label={i18nStrings.propertyText} className={styles['token-editor-field-property']}>
265-
<PropertyInput
266-
property={property}
267-
onChangePropertyKey={onChangePropertyKey}
268-
asyncProps={asyncProperties ? asyncProps : null}
269-
filteringProperties={filteringProperties}
270-
onLoadItems={onLoadItems}
271-
customGroupsText={customGroupsText}
272-
i18nStrings={i18nStrings}
273-
freeTextFiltering={freeTextFiltering}
286+
<InternalBox margin={{ bottom: 's' }}>
287+
{/* TODO: i18n */}
288+
<InternalFormField label="Operation">
289+
<InternalSelect
290+
options={operationOptions}
291+
selectedOption={operationOptions.find(option => option.value === operation)!}
292+
onChange={event => setOperation(event.detail.selectedOption.value as 'and' | 'or')}
274293
/>
275294
</InternalFormField>
295+
</InternalBox>
276296

277-
<InternalFormField label={i18nStrings.operatorText} className={styles['token-editor-field-operator']}>
278-
<OperatorInput
279-
property={property}
280-
operator={operator}
281-
onChangeOperator={onChangeOperator}
282-
i18nStrings={i18nStrings}
283-
freeTextFiltering={freeTextFiltering}
284-
/>
285-
</InternalFormField>
297+
{groups.map((group, index) => (
298+
<div key={index} className={styles['token-editor-form']}>
299+
<InternalFormField label={i18nStrings.propertyText} className={styles['token-editor-field-property']}>
300+
<PropertyInput
301+
property={group.property}
302+
onChangePropertyKey={group.onChangePropertyKey}
303+
asyncProps={asyncProperties ? asyncProps : null}
304+
filteringProperties={filteringProperties}
305+
onLoadItems={onLoadItems}
306+
customGroupsText={customGroupsText}
307+
i18nStrings={i18nStrings}
308+
freeTextFiltering={freeTextFiltering}
309+
/>
310+
</InternalFormField>
286311

287-
<InternalFormField label={i18nStrings.valueText} className={styles['token-editor-field-value']}>
288-
<ValueInput
289-
property={property}
290-
operator={operator}
291-
value={value}
292-
onChangeValue={onChangeValue}
293-
asyncProps={asyncProps}
294-
filteringOptions={filteringOptions}
295-
onLoadItems={onLoadItems}
296-
i18nStrings={i18nStrings}
297-
/>
298-
</InternalFormField>
299-
</div>
312+
<InternalFormField label={i18nStrings.operatorText} className={styles['token-editor-field-operator']}>
313+
<OperatorInput
314+
property={group.property}
315+
operator={group.operator}
316+
onChangeOperator={group.onChangeOperator}
317+
i18nStrings={i18nStrings}
318+
freeTextFiltering={freeTextFiltering}
319+
/>
320+
</InternalFormField>
321+
322+
<InternalFormField label={i18nStrings.valueText} className={styles['token-editor-field-value']}>
323+
<ValueInput
324+
property={group.property}
325+
operator={group.operator}
326+
value={group.value}
327+
onChangeValue={group.onChangeValue}
328+
asyncProps={asyncProps}
329+
filteringOptions={filteringOptions}
330+
onLoadItems={onLoadItems}
331+
i18nStrings={i18nStrings}
332+
/>
333+
</InternalFormField>
334+
335+
{/* TODO: i18n */}
336+
{groups.length > 1 && (
337+
<InternalBox margin={{ top: 'xs' }}>
338+
<InternalButton
339+
iconName="remove"
340+
onClick={() =>
341+
setTemp(prev => {
342+
const copy = [...prev];
343+
copy.splice(index, 1);
344+
return copy;
345+
})
346+
}
347+
>
348+
Remove token
349+
</InternalButton>
350+
</InternalBox>
351+
)}
352+
</div>
353+
))}
354+
355+
{/* TODO: i18n */}
356+
<InternalButton
357+
iconName="add-plus"
358+
onClick={() => setTemp(prev => [...prev, { property: null, operator: ':', value: null }])}
359+
>
360+
Add token
361+
</InternalButton>
300362

301363
<div className={styles['token-editor-actions']}>
302364
<InternalButton
@@ -311,7 +373,10 @@ export function TokenEditor({
311373
className={styles['token-editor-submit']}
312374
formAction="none"
313375
onClick={() => {
314-
setToken({ operation: token.operation, tokens: [matchTokenValue(temporaryToken, filteringOptions)] });
376+
setToken({
377+
operation,
378+
tokens: temp.map(temporaryToken => matchTokenValue(temporaryToken, filteringOptions)),
379+
});
315380
closePopover();
316381
}}
317382
>

src/property-filter/utils.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,7 @@ export function getFormattedToken(group: InternalTokenGroup) {
105105
const propertyLabel = firstToken.property && firstToken.property.propertyLabel;
106106
const tokenValue = valueFormatter ? valueFormatter(firstToken.value) : firstToken.value;
107107
// TODO: use i18n
108-
const suffix =
109-
firstLevelTokens.length > 1 ? ` ${group.operation} ${firstLevelTokens.length - 1} more properties` : '';
108+
const suffix = firstLevelTokens.length > 1 ? ` ${group.operation}${firstLevelTokens.length - 1} more` : '';
110109
const label = `${propertyLabel ?? ''} ${firstToken.operator} ${tokenValue}${suffix}`;
111110
return { property: propertyLabel ?? '', operator: firstToken.operator, value: tokenValue, suffix, label };
112111
}

0 commit comments

Comments
 (0)