-
Notifications
You must be signed in to change notification settings - Fork 901
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(core): Convert Cluster filters to react (#8272)
* refactor(core): Convert cluster filters to react * refactor(core): Angular backwards compatbility & code cleanup * Make input controlled * refactor(core): useMemo for labelsMap * refactor(core): Tweaks for performance * Clear label filters
- Loading branch information
1 parent
02bb701
commit f3cc20f
Showing
9 changed files
with
420 additions
and
484 deletions.
There are no files selected for viewing
340 changes: 340 additions & 0 deletions
340
app/scripts/modules/core/src/cluster/filter/ClusterFilters.tsx
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,340 @@ | ||
import React from 'react'; | ||
import { useOnStateChanged, StateDeclaration } from '@uirouter/react'; | ||
import { compact, uniq, map } from 'lodash'; | ||
|
||
import { Application } from 'core/application'; | ||
import { ClusterState } from 'core/state'; | ||
import { FilterCheckbox, ISortFilter, digestDependentFilters } from 'core/filterModel'; | ||
import { robotToHuman, useDataSource, useObservable } from 'core/presentation'; | ||
import { | ||
buildLabelsMap, | ||
labelFiltersToTrueKeyObject, | ||
trueKeyObjectToLabelFilters, | ||
ILabelFilter, | ||
} from './labelFilterUtils'; | ||
|
||
import { poolBuilder } from './clusterDependentFilterHelper.service'; | ||
import { FilterSection } from './FilterSection'; | ||
import LabelFilter from './LabelFilter'; | ||
|
||
export interface IClusterFiltersProps { | ||
app: Application; | ||
} | ||
|
||
interface IClusterHeaders { | ||
account: string[]; | ||
availabilityZone: string[]; | ||
category: string[]; | ||
detail: string[]; | ||
instanceType: string[]; | ||
providerType: string[]; | ||
region: string[]; | ||
stack: string[]; | ||
} | ||
|
||
export const ClusterFilters = ({ app }: IClusterFiltersProps) => { | ||
const { serverGroups } = app; | ||
const { data: serverGroupData, loaded: clustersLoaded } = useDataSource(serverGroups); | ||
|
||
const [tags, setTags] = React.useState(ClusterState.filterModel.asFilterModel.tags); | ||
const [sortFilter, setSortFilter] = React.useState<ISortFilter>(ClusterState.filterModel.asFilterModel.sortFilter); | ||
const [labelFilters, setLabelFilters] = React.useState<ILabelFilter[]>( | ||
trueKeyObjectToLabelFilters(sortFilter.labels), | ||
); | ||
|
||
const labelsMap = React.useMemo(() => buildLabelsMap(serverGroupData), [serverGroupData.length]); | ||
const showLabelFilter = Object.keys(labelsMap).length > 0; | ||
|
||
const getHeadingsForOption = (option: string): string[] => | ||
compact(uniq(map(serverGroupData, option) as string[])).sort(); | ||
const [headings, setHeadings] = React.useState<IClusterHeaders>({ | ||
account: [], | ||
availabilityZone: [], | ||
category: getHeadingsForOption('category'), | ||
detail: ['(none)'].concat(getHeadingsForOption('detail')), | ||
instanceType: [], | ||
providerType: [], | ||
region: [], | ||
stack: ['(none)'].concat(getHeadingsForOption('stack')), | ||
}); | ||
|
||
useObservable(ClusterState.filterService.groupsUpdatedStream, () => { | ||
setTags(ClusterState.filterModel.asFilterModel.tags); | ||
setSortFilter(ClusterState.filterModel.asFilterModel.sortFilter); | ||
setLabelFilters(trueKeyObjectToLabelFilters(ClusterState.filterModel.asFilterModel.sortFilter.labels)); | ||
}); | ||
|
||
useOnStateChanged((state: StateDeclaration) => { | ||
if (state.name.includes('clusters')) { | ||
ClusterState.filterModel.asFilterModel.activate(); | ||
} | ||
}); | ||
|
||
const clearFilters = () => { | ||
setSortFilter({ | ||
...sortFilter, | ||
minInstances: undefined, | ||
maxInstances: undefined, | ||
}); | ||
setLabelFilters([]); | ||
ClusterState.filterService.clearFilters(); | ||
ClusterState.filterModel.asFilterModel.applyParamsToUrl(); | ||
ClusterState.filterService.updateClusterGroups(app); | ||
}; | ||
|
||
const updateClusterGroups = (applyParamsToUrl = true) => { | ||
const { providerType, instanceType, account, availabilityZone, region } = digestDependentFilters({ | ||
sortFilter: ClusterState.filterModel.asFilterModel.sortFilter, | ||
dependencyOrder: ['providerType', 'account', 'region', 'availabilityZone', 'instanceType'], | ||
pool: poolBuilder(serverGroupData), | ||
}); | ||
|
||
setHeadings({ | ||
account, | ||
availabilityZone, | ||
category: getHeadingsForOption('category'), | ||
detail: ['(none)'].concat(getHeadingsForOption('detail')), | ||
instanceType, | ||
providerType, | ||
region, | ||
stack: ['(none)'].concat(getHeadingsForOption('stack')), | ||
}); | ||
|
||
if (applyParamsToUrl) { | ||
ClusterState.filterModel.asFilterModel.applyParamsToUrl(); | ||
} | ||
ClusterState.filterService.updateClusterGroups(app); | ||
}; | ||
|
||
const handleLabelFiltersChange = (filters: ILabelFilter[]): void => { | ||
const newSortFilter = { | ||
...sortFilter, | ||
labels: labelFiltersToTrueKeyObject(filters), | ||
}; | ||
|
||
setLabelFilters(filters); | ||
setSortFilter(newSortFilter); | ||
ClusterState.filterModel.asFilterModel.sortFilter = newSortFilter; | ||
updateClusterGroups(); | ||
}; | ||
|
||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
const searchTerm = event.target.value; | ||
const newSortFilter = { | ||
...sortFilter, | ||
filter: searchTerm, | ||
}; | ||
setSortFilter(newSortFilter); | ||
ClusterState.filterModel.asFilterModel.sortFilter = newSortFilter; | ||
updateClusterGroups(); | ||
}; | ||
|
||
const handleStatusChange = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
const target = event.target; | ||
const value = target.type === 'checkbox' ? target.checked : target.value; | ||
const name = target.name; | ||
|
||
const newSortFilter = { | ||
...sortFilter, | ||
status: { | ||
...sortFilter.status, | ||
[name]: Boolean(value), | ||
}, | ||
}; | ||
setSortFilter(newSortFilter); | ||
ClusterState.filterModel.asFilterModel.sortFilter = newSortFilter; | ||
updateClusterGroups(); | ||
}; | ||
|
||
const handleMinInstanceChange = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
const numInstances = event.target.value; | ||
const min = numInstances ? parseInt(numInstances, 10) : undefined; | ||
const newSortFilter = { | ||
...sortFilter, | ||
minInstances: min, | ||
}; | ||
setSortFilter(newSortFilter); | ||
ClusterState.filterModel.asFilterModel.sortFilter = newSortFilter; | ||
updateClusterGroups(); | ||
}; | ||
|
||
const handleMaxInstanceChange = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
const numInstances = event.target.value; | ||
const max = numInstances ? parseInt(numInstances, 10) : undefined; | ||
const newSortFilter = { | ||
...sortFilter, | ||
maxInstances: max, | ||
}; | ||
setSortFilter(newSortFilter); | ||
ClusterState.filterModel.asFilterModel.sortFilter = newSortFilter; | ||
updateClusterGroups(); | ||
}; | ||
|
||
React.useEffect(() => { | ||
updateClusterGroups(); | ||
}, [serverGroupData.length]); | ||
|
||
return ( | ||
<div className="insight-filter-content"> | ||
<div className="heading"> | ||
<span | ||
className="btn btn-default btn-xs" | ||
style={{ visibility: tags.length > 0 ? 'inherit' : 'hidden' }} | ||
onClick={clearFilters} | ||
> | ||
Clear All | ||
</span> | ||
<FilterSection key="filter-search" heading="Search" expanded={true} helpKey="cluster.search"> | ||
<form className="form-horizontal" role="form"> | ||
<div className="form-group nav-search"> | ||
<input | ||
type="search" | ||
className="form-control input-sm" | ||
value={sortFilter.filter} | ||
onBlur={handleSearchChange} | ||
onChange={handleSearchChange} | ||
style={{ width: '85%', display: 'inline-block' }} | ||
/> | ||
</div> | ||
</form> | ||
</FilterSection> | ||
</div> | ||
{clustersLoaded && ( | ||
<div className="content"> | ||
{headings.providerType.length > 1 && ( | ||
<FilterSection key="filter-provider" heading="Provider" expanded={true}> | ||
{headings.providerType.map(heading => ( | ||
<FilterCheckbox | ||
heading={heading} | ||
isCloudProvider={true} | ||
key={heading} | ||
sortFilterType={sortFilter.providerType} | ||
onChange={updateClusterGroups} | ||
/> | ||
))} | ||
</FilterSection> | ||
)} | ||
<FilterSection key="filter-account" heading="Account" expanded={true}> | ||
{headings.account.map(heading => ( | ||
<FilterCheckbox | ||
heading={heading} | ||
key={heading} | ||
sortFilterType={sortFilter.account} | ||
onChange={updateClusterGroups} | ||
/> | ||
))} | ||
</FilterSection> | ||
<FilterSection key="filter-region" heading="Region" expanded={true}> | ||
{headings.region.map(heading => ( | ||
<FilterCheckbox | ||
heading={heading} | ||
key={heading} | ||
sortFilterType={sortFilter.region} | ||
onChange={updateClusterGroups} | ||
/> | ||
))} | ||
</FilterSection> | ||
<FilterSection key="filter-category" heading="Category" expanded={true}> | ||
{headings.category.map(heading => ( | ||
<FilterCheckbox | ||
heading={robotToHuman(heading)} | ||
key={heading} | ||
sortFilterType={sortFilter.category} | ||
onChange={updateClusterGroups} | ||
/> | ||
))} | ||
</FilterSection> | ||
<FilterSection key="filter-stack" heading="Stack" expanded={true}> | ||
{headings.stack.map(heading => ( | ||
<FilterCheckbox | ||
heading={heading} | ||
key={heading} | ||
sortFilterType={sortFilter.stack} | ||
onChange={updateClusterGroups} | ||
/> | ||
))} | ||
</FilterSection> | ||
<FilterSection key="filter-detail" heading="Detail" expanded={true}> | ||
{headings.detail.map(heading => ( | ||
<FilterCheckbox | ||
heading={heading} | ||
key={heading} | ||
sortFilterType={sortFilter.detail} | ||
onChange={updateClusterGroups} | ||
/> | ||
))} | ||
</FilterSection> | ||
<FilterSection key="filter-status" heading="Status" expanded={true}> | ||
<div className="form"> | ||
{['Up', 'Down', 'Disabled', 'Starting', 'OutOfService', 'Unknown'].map(status => ( | ||
<div className="checkbox"> | ||
<label> | ||
<input | ||
key={status} | ||
type="checkbox" | ||
checked={Boolean(sortFilter.status && sortFilter.status[status])} | ||
onChange={handleStatusChange} | ||
name={status} | ||
/> | ||
{robotToHuman(status)} | ||
</label> | ||
</div> | ||
))} | ||
</div> | ||
</FilterSection> | ||
<FilterSection key="filter-az" heading="Availability Zones" expanded={true}> | ||
{headings.availabilityZone.map(heading => ( | ||
<FilterCheckbox | ||
heading={heading} | ||
key={heading} | ||
sortFilterType={sortFilter.availabilityZone} | ||
onChange={updateClusterGroups} | ||
/> | ||
))} | ||
</FilterSection> | ||
<FilterSection key="filter-instance-types" heading="Instance Types" expanded={true}> | ||
{headings.instanceType.map(heading => ( | ||
<FilterCheckbox | ||
heading={heading} | ||
key={heading} | ||
sortFilterType={sortFilter.instanceType} | ||
onChange={updateClusterGroups} | ||
/> | ||
))} | ||
</FilterSection> | ||
<FilterSection key="filter-instance-count" heading="Instance Count" expanded={true}> | ||
<div className="form-inline"> | ||
<div className="form-group"> | ||
{'Min: '} | ||
<input | ||
type="number" | ||
className="form-control input-sm" | ||
value={sortFilter.minInstances || ''} | ||
onChange={handleMinInstanceChange} | ||
/> | ||
</div> | ||
<div className="form-group"> | ||
{'Max: '} | ||
<input | ||
type="number" | ||
className="form-control input-sm" | ||
value={sortFilter.maxInstances || ''} | ||
onChange={handleMaxInstanceChange} | ||
/> | ||
</div> | ||
</div> | ||
</FilterSection> | ||
{showLabelFilter && ( | ||
<FilterSection key="filter-label" heading="Labels" expanded={true}> | ||
<LabelFilter | ||
labelsMap={labelsMap} | ||
labelFilters={labelFilters} | ||
updateLabelFilters={handleLabelFiltersChange} | ||
/> | ||
</FilterSection> | ||
)} | ||
</div> | ||
)} | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.