-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #64 from rszwajko/providersV4
Provide reusable components for creating standard list page
- Loading branch information
Showing
34 changed files
with
2,451 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,23 @@ | ||
{ | ||
"Cancel": "Cancel", | ||
"Clear all filters": "Clear all filters", | ||
"Filter by name": "Filter by name", | ||
"Filter by namespace": "Filter by namespace", | ||
"Loading": "Loading", | ||
"Manage Columns": "Manage Columns", | ||
"Mappings for VM Import": "Mappings for VM Import", | ||
"Name": "Name", | ||
"Namespace": "Namespace", | ||
"No results found": "No results found", | ||
"No results match the filter criteria. Clear all filters and try again.": "No results match the filter criteria. Clear all filters and try again.", | ||
"Plans for VM Import": "Plans for VM Import", | ||
"Providers for VM Import": "Providers for VM Import", | ||
"Reorder": "Reorder", | ||
"Restore default colums": "Restore default colums", | ||
"Save": "Save", | ||
"Select Filter": "Select Filter", | ||
"Selected columns will be displayed in the table.": "Selected columns will be displayed in the table.", | ||
"Table column management": "Table column management", | ||
"Unable to retrieve data": "Unable to retrieve data", | ||
"Virtualization": "Virtualization" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,4 +5,7 @@ | |
*/ | ||
export const useTranslation = () => ({ | ||
t: (k: string) => k, | ||
i18n: { | ||
resolvedLanguage: 'en', | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import React, { useState } from 'react'; | ||
import { useTranslation } from 'src/internal/i18n'; | ||
|
||
import { | ||
Select, | ||
SelectOption, | ||
SelectOptionObject, | ||
SelectVariant, | ||
ToolbarGroup, | ||
ToolbarItem, | ||
} from '@patternfly/react-core'; | ||
|
||
import { MetaFilterProps } from './types'; | ||
|
||
interface IdOption extends SelectOptionObject { | ||
id: string; | ||
} | ||
|
||
const toSelectOption = (id: string, label: string): IdOption => ({ | ||
id, | ||
compareTo: (other: IdOption): boolean => id === other?.id, | ||
toString: () => label, | ||
}); | ||
|
||
/** | ||
* Implementation of PatternFly 4 attribute-value filter pattern. | ||
* Accepts any filter matching FilterTypeProps interface. | ||
* | ||
* @see FilterTypeProps | ||
*/ | ||
export const AttributeValueFilter = ({ | ||
selectedFilters, | ||
onFilterUpdate, | ||
fieldFilters, | ||
supportedFilterTypes = {}, | ||
}: MetaFilterProps) => { | ||
const { t } = useTranslation(); | ||
const [currentFilter, setCurrentFilter] = useState(fieldFilters?.[0]); | ||
const [expanded, setExpanded] = useState(false); | ||
|
||
const selectOptionToFilter = (selectedId) => | ||
fieldFilters.find(({ fieldId }) => fieldId === selectedId) ?? currentFilter; | ||
|
||
const onFilterTypeSelect = (event, value, isPlaceholder) => { | ||
if (!isPlaceholder) { | ||
setCurrentFilter(selectOptionToFilter(value?.id)); | ||
setExpanded(!expanded); | ||
} | ||
}; | ||
|
||
return ( | ||
<ToolbarGroup variant="filter-group"> | ||
<ToolbarItem> | ||
<Select | ||
onSelect={onFilterTypeSelect} | ||
onToggle={setExpanded} | ||
isOpen={expanded} | ||
variant={SelectVariant.single} | ||
aria-label={t('Select Filter')} | ||
selections={ | ||
currentFilter && toSelectOption(currentFilter.fieldId, currentFilter.toFieldLabel(t)) | ||
} | ||
> | ||
{fieldFilters.map(({ fieldId, toFieldLabel }) => ( | ||
<SelectOption key={fieldId} value={toSelectOption(fieldId, toFieldLabel(t))} /> | ||
))} | ||
</Select> | ||
</ToolbarItem> | ||
|
||
{fieldFilters.map(({ fieldId: id, toFieldLabel, filterDef: filter }) => { | ||
const FilterType = supportedFilterTypes[filter.type]; | ||
return ( | ||
FilterType && ( | ||
<FilterType | ||
key={id} | ||
filterId={id} | ||
onFilterUpdate={(values) => | ||
onFilterUpdate({ | ||
...selectedFilters, | ||
[id]: values, | ||
}) | ||
} | ||
placeholderLabel={filter.toPlaceholderLabel(t)} | ||
selectedFilters={selectedFilters[id] ?? []} | ||
showFilter={currentFilter?.fieldId === id} | ||
title={filter?.toLabel?.(t) ?? toFieldLabel(t)} | ||
supportedValues={filter.values} | ||
/> | ||
) | ||
); | ||
})} | ||
</ToolbarGroup> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import React, { useMemo, useState } from 'react'; | ||
import { useTranslation } from 'src/internal/i18n'; | ||
import { localeCompare } from 'src/utils/helpers'; | ||
|
||
import { | ||
Select, | ||
SelectOption, | ||
SelectOptionObject, | ||
SelectVariant, | ||
ToolbarChip, | ||
ToolbarFilter, | ||
} from '@patternfly/react-core'; | ||
|
||
import { FilterTypeProps } from './types'; | ||
|
||
/** | ||
* One label may map to multiple enum ids due to translation or by design (i.e. "Unknown") | ||
* Aggregate enums with the same label and display them as a single option. | ||
* | ||
* @returns { uniqueEnumLabels, onUniqueFilterUpdate, selectedUniqueEnumLabels }; | ||
*/ | ||
export const useUnique = ({ | ||
supportedEnumValues, | ||
onSelectedEnumIdsChange, | ||
selectedEnumIds, | ||
}: { | ||
supportedEnumValues: { | ||
id: string; | ||
toLabel(t: (key: string) => string): string; | ||
}[]; | ||
onSelectedEnumIdsChange: (values: string[]) => void; | ||
selectedEnumIds: string[]; | ||
}): { | ||
uniqueEnumLabels: string[]; | ||
onUniqueFilterUpdate: (selectedEnumLabels: string[]) => void; | ||
selectedUniqueEnumLabels: string[]; | ||
} => { | ||
const { t, i18n } = useTranslation(); | ||
|
||
const translatedEnums = useMemo( | ||
() => | ||
supportedEnumValues.map((it) => ({ | ||
// fallback to ID | ||
label: it.toLabel?.(t) ?? it.id, | ||
id: it.id, | ||
})), | ||
|
||
[supportedEnumValues], | ||
); | ||
|
||
// group filters with the same label | ||
const labelToIds = useMemo( | ||
() => | ||
translatedEnums.reduce((acc, { label, id }) => { | ||
acc[label] = [...(acc?.[label] ?? []), id]; | ||
return acc; | ||
}, {}), | ||
[translatedEnums], | ||
); | ||
|
||
// for easy reverse lookup | ||
const idToLabel = useMemo( | ||
() => | ||
translatedEnums.reduce((acc, { label, id }) => { | ||
acc[id] = label; | ||
return acc; | ||
}, {}), | ||
[translatedEnums], | ||
); | ||
|
||
const uniqueEnumLabels = useMemo( | ||
() => | ||
Object.entries(labelToIds) | ||
.map(([label]) => label) | ||
.sort((a, b) => localeCompare(a, b, i18n.resolvedLanguage)), | ||
[labelToIds], | ||
); | ||
|
||
const onUniqueFilterUpdate = useMemo( | ||
() => | ||
(labels: string[]): void => | ||
onSelectedEnumIdsChange(labels.flatMap((label) => labelToIds[label] ?? [])), | ||
[onSelectedEnumIdsChange, labelToIds], | ||
); | ||
|
||
const selectedUniqueEnumLabels = useMemo( | ||
() => [...new Set(selectedEnumIds.map((id) => idToLabel[id]).filter(Boolean))] as string[], | ||
[selectedEnumIds, idToLabel], | ||
); | ||
|
||
return { uniqueEnumLabels, onUniqueFilterUpdate, selectedUniqueEnumLabels }; | ||
}; | ||
|
||
/** | ||
* Select one or many enum values from the list. | ||
* FilterTypeProps are interpeted as follows: | ||
* 1) selectedFilters - selected enum IDs (not translated constant identifiers) | ||
* 2) onFilterUpdate - accepts the list of selected enum IDs | ||
* 3) supportedValues - supported enum values | ||
*/ | ||
export const EnumFilter = ({ | ||
selectedFilters: selectedEnumIds = [], | ||
onFilterUpdate: onSelectedEnumIdsChange, | ||
supportedValues: supportedEnumValues = [], | ||
title, | ||
placeholderLabel, | ||
filterId, | ||
showFilter, | ||
}: FilterTypeProps) => { | ||
const [isExpanded, setExpanded] = useState(false); | ||
const { uniqueEnumLabels, onUniqueFilterUpdate, selectedUniqueEnumLabels } = useUnique({ | ||
supportedEnumValues, | ||
onSelectedEnumIdsChange, | ||
selectedEnumIds, | ||
}); | ||
|
||
const deleteFilter = (label: string | ToolbarChip | SelectOptionObject): void => | ||
onUniqueFilterUpdate(selectedUniqueEnumLabels.filter((filterLabel) => filterLabel !== label)); | ||
|
||
const hasFilter = (label: string | SelectOptionObject): boolean => | ||
!!selectedUniqueEnumLabels.find((filterLabel) => filterLabel === label); | ||
|
||
const addFilter = (label: string | SelectOptionObject): void => { | ||
if (typeof label === 'string') { | ||
onUniqueFilterUpdate([...selectedUniqueEnumLabels, label]); | ||
} | ||
}; | ||
|
||
return ( | ||
<ToolbarFilter | ||
key={filterId} | ||
chips={selectedUniqueEnumLabels} | ||
deleteChip={(category, option) => deleteFilter(option)} | ||
deleteChipGroup={() => onUniqueFilterUpdate([])} | ||
categoryName={title} | ||
showToolbarItem={showFilter} | ||
> | ||
<Select | ||
variant={SelectVariant.checkbox} | ||
aria-label={placeholderLabel} | ||
onSelect={(event, option, isPlaceholder) => { | ||
if (isPlaceholder) { | ||
return; | ||
} | ||
hasFilter(option) ? deleteFilter(option) : addFilter(option); | ||
}} | ||
selections={selectedUniqueEnumLabels} | ||
placeholderText={placeholderLabel} | ||
isOpen={isExpanded} | ||
onToggle={setExpanded} | ||
> | ||
{uniqueEnumLabels.map((label) => ( | ||
<SelectOption key={label} value={label} /> | ||
))} | ||
</Select> | ||
</ToolbarFilter> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import React, { useState } from 'react'; | ||
|
||
import { InputGroup, SearchInput, ToolbarFilter } from '@patternfly/react-core'; | ||
|
||
import { FilterTypeProps } from './types'; | ||
|
||
/** | ||
* Filter using text provided by the user. | ||
* Text needs to be submitted/confirmed by clicking search button or by pressing Enter key. | ||
* | ||
* FilterTypeProps are interpeted as follows: | ||
* 1) selectedFilters - list of strings provided by the user | ||
* 2) onFilterUpdate - accepts the list of strings (from user input) | ||
*/ | ||
export const FreetextFilter = ({ | ||
filterId, | ||
selectedFilters, | ||
onFilterUpdate, | ||
title, | ||
showFilter, | ||
placeholderLabel, | ||
}: FilterTypeProps) => { | ||
const [inputValue, setInputValue] = useState(''); | ||
const onTextInput = (): void => { | ||
if (!inputValue || selectedFilters.includes(inputValue)) { | ||
return; | ||
} | ||
onFilterUpdate([...selectedFilters, inputValue]); | ||
setInputValue(''); | ||
}; | ||
return ( | ||
<ToolbarFilter | ||
key={filterId} | ||
chips={selectedFilters ?? []} | ||
deleteChip={(category, option) => | ||
onFilterUpdate(selectedFilters?.filter((value) => value !== option) ?? []) | ||
} | ||
deleteChipGroup={() => onFilterUpdate([])} | ||
categoryName={title} | ||
showToolbarItem={showFilter} | ||
> | ||
<InputGroup> | ||
<SearchInput | ||
placeholder={placeholderLabel} | ||
value={inputValue} | ||
onChange={setInputValue} | ||
onSearch={onTextInput} | ||
onClear={() => setInputValue('')} | ||
/> | ||
</InputGroup> | ||
</ToolbarFilter> | ||
); | ||
}; |
Oops, something went wrong.