-
Notifications
You must be signed in to change notification settings - Fork 7
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
Add multi-filtering to tables; refactor details pages into tabs #63
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
.multi-select-filter { | ||
&--width { | ||
width: 150px; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
export { MultiSelectFilter } from './multi_select_filter'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import React, { useState } from 'react'; | ||
import { | ||
EuiFilterSelectItem, | ||
EuiFilterGroup, | ||
EuiPopover, | ||
EuiFilterButton, | ||
EuiFlexItem, | ||
} from '@elastic/eui'; | ||
|
||
// styling | ||
import './general-component-styles.scss'; | ||
|
||
interface MultiSelectFilterProps { | ||
title: string; | ||
filters: EuiFilterSelectItem[]; | ||
setSelectedFilters: (filters: EuiFilterSelectItem[]) => void; | ||
} | ||
|
||
/** | ||
* A general multi-select filter. | ||
*/ | ||
export function MultiSelectFilter(props: MultiSelectFilterProps) { | ||
const [filters, setFilters] = useState(props.filters); | ||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false); | ||
|
||
function onButtonClick() { | ||
setIsPopoverOpen(!isPopoverOpen); | ||
} | ||
function onPopoverClose() { | ||
setIsPopoverOpen(false); | ||
} | ||
|
||
function updateFilter(index: number) { | ||
if (!filters[index]) { | ||
return; | ||
} | ||
const newFilters = [...filters]; | ||
// @ts-ignore | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TLDR of adding all of these ignores is due to IDE complaints that the type fields don't match. This is due to differences in EUI vs. OUI and their respective documentation. This follows the latest OUI documentation. |
||
newFilters[index].checked = | ||
// @ts-ignore | ||
newFilters[index].checked === 'on' ? undefined : 'on'; | ||
|
||
setFilters(newFilters); | ||
props.setSelectedFilters( | ||
// @ts-ignore | ||
newFilters.filter((filter) => filter.checked === 'on') | ||
); | ||
} | ||
|
||
return ( | ||
<EuiFlexItem grow={false} className="multi-select-filter--width"> | ||
<EuiFilterGroup> | ||
<EuiPopover | ||
button={ | ||
<EuiFilterButton | ||
iconType="arrowDown" | ||
onClick={onButtonClick} | ||
isSelected={isPopoverOpen} | ||
numFilters={filters.length} | ||
hasActiveFilters={ | ||
// @ts-ignore | ||
!!filters.find((filter) => filter.checked === 'on') | ||
} | ||
numActiveFilters={ | ||
// @ts-ignore | ||
filters.filter((filter) => filter.checked === 'on').length | ||
} | ||
> | ||
{props.title} | ||
</EuiFilterButton> | ||
} | ||
isOpen={isPopoverOpen} | ||
closePopover={onPopoverClose} | ||
panelPaddingSize="none" | ||
> | ||
<div className="euiFilterSelect__items multi-select-filter--width"> | ||
{filters.map((filter, index) => ( | ||
<EuiFilterSelectItem | ||
// @ts-ignore | ||
checked={filter.checked} | ||
key={index} | ||
onClick={() => updateFilter(index)} | ||
> | ||
{/* @ts-ignore */} | ||
{filter.name} | ||
</EuiFilterSelectItem> | ||
))} | ||
</div> | ||
</EuiPopover> | ||
</EuiFilterGroup> | ||
</EuiFlexItem> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ import { saveWorkflow } from '../utils'; | |
import { rfContext, AppState, removeDirty } from '../../../store'; | ||
|
||
interface WorkflowDetailHeaderProps { | ||
tabs: any[]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
workflow?: Workflow; | ||
} | ||
|
||
|
@@ -22,7 +23,6 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) { | |
return ( | ||
<EuiPageHeader | ||
pageTitle={props.workflow ? props.workflow.name : ''} | ||
description={props.workflow ? props.workflow.description : ''} | ||
rightSideItems={[ | ||
<EuiButton fill={false} onClick={() => {}}> | ||
Prototype | ||
|
@@ -39,6 +39,8 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) { | |
Save | ||
</EuiButton>, | ||
]} | ||
tabs={props.tabs} | ||
bottomBorder={true} | ||
/> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
export const columns = [ | ||
{ | ||
field: 'id', | ||
name: 'Launch ID', | ||
sortable: true, | ||
}, | ||
{ | ||
field: 'state', | ||
name: 'Status', | ||
sortable: true, | ||
}, | ||
{ | ||
field: 'lastUpdatedTime', | ||
name: 'Last updated time', | ||
sortable: true, | ||
}, | ||
]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
export { Launches } from './launches'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import React from 'react'; | ||
import { EuiText } from '@elastic/eui'; | ||
|
||
interface LaunchDetailsProps {} | ||
|
||
export function LaunchDetails(props: LaunchDetailsProps) { | ||
return <EuiText>TODO: add selected launch details here</EuiText>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import React, { useState, useEffect } from 'react'; | ||
import { debounce } from 'lodash'; | ||
import { | ||
EuiInMemoryTable, | ||
Direction, | ||
EuiFlexGroup, | ||
EuiFlexItem, | ||
EuiFieldSearch, | ||
EuiFilterSelectItem, | ||
} from '@elastic/eui'; | ||
import { WORKFLOW_STATE, WorkflowLaunch } from '../../../../common'; | ||
import { columns } from './columns'; | ||
import { MultiSelectFilter } from '../../../general_components'; | ||
import { getStateOptions } from '../../../utils'; | ||
|
||
interface LaunchListProps {} | ||
|
||
/** | ||
* The searchable list of launches for this particular workflow. | ||
*/ | ||
export function LaunchList(props: LaunchListProps) { | ||
// TODO: finalize how we persist launches for a particular workflow. | ||
// We may just add UI metadata tags to group workflows under a single, overall "workflow" | ||
// const { workflows } = useSelector((state: AppState) => state.workflows); | ||
const workflowLaunches = [ | ||
{ | ||
id: 'Launch_1', | ||
state: WORKFLOW_STATE.IN_PROGRESS, | ||
lastUpdated: 12345678, | ||
}, | ||
{ | ||
id: 'Launch_2', | ||
state: WORKFLOW_STATE.FAILED, | ||
lastUpdated: 12345677, | ||
}, | ||
] as WorkflowLaunch[]; | ||
|
||
// search bar state | ||
const [searchQuery, setSearchQuery] = useState<string>(''); | ||
const debounceSearchQuery = debounce((query: string) => { | ||
setSearchQuery(query); | ||
}, 100); | ||
|
||
// filters state | ||
const [selectedStates, setSelectedStates] = useState<EuiFilterSelectItem[]>( | ||
getStateOptions() | ||
); | ||
const [filteredLaunches, setFilteredLaunches] = useState<WorkflowLaunch[]>( | ||
workflowLaunches | ||
); | ||
|
||
// When a filter selection or search query changes, update the filtered launches | ||
useEffect(() => { | ||
setFilteredLaunches( | ||
fetchFilteredLaunches(workflowLaunches, selectedStates, searchQuery) | ||
); | ||
}, [selectedStates, searchQuery]); | ||
|
||
const sorting = { | ||
sort: { | ||
field: 'id', | ||
direction: 'asc' as Direction, | ||
}, | ||
}; | ||
|
||
return ( | ||
<EuiFlexGroup direction="column"> | ||
<EuiFlexItem> | ||
<EuiFlexGroup direction="row" gutterSize="m"> | ||
<EuiFlexItem grow={true}> | ||
<EuiFieldSearch | ||
fullWidth={true} | ||
placeholder="Search launches..." | ||
onChange={(e) => debounceSearchQuery(e.target.value)} | ||
/> | ||
</EuiFlexItem> | ||
<MultiSelectFilter | ||
filters={getStateOptions()} | ||
title="Status" | ||
setSelectedFilters={setSelectedStates} | ||
/> | ||
</EuiFlexGroup> | ||
</EuiFlexItem> | ||
<EuiFlexItem> | ||
<EuiInMemoryTable<WorkflowLaunch> | ||
items={filteredLaunches} | ||
rowHeader="id" | ||
columns={columns} | ||
sorting={sorting} | ||
pagination={true} | ||
message={'No existing launches found'} | ||
/> | ||
</EuiFlexItem> | ||
</EuiFlexGroup> | ||
); | ||
} | ||
|
||
// Collect the final launch list after applying all filters | ||
function fetchFilteredLaunches( | ||
allLaunches: WorkflowLaunch[], | ||
stateFilters: EuiFilterSelectItem[], | ||
searchQuery: string | ||
): WorkflowLaunch[] { | ||
// @ts-ignore | ||
const stateFilterStrings = stateFilters.map((filter) => filter.name); | ||
const filteredLaunches = allLaunches.filter((launch) => | ||
stateFilterStrings.includes(launch.state) | ||
); | ||
return searchQuery.length === 0 | ||
? filteredLaunches | ||
: filteredLaunches.filter((launch) => | ||
launch.id.toLowerCase().includes(searchQuery.toLowerCase()) | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import React from 'react'; | ||
import { | ||
EuiFlexGroup, | ||
EuiFlexItem, | ||
EuiPageContent, | ||
EuiSpacer, | ||
EuiTitle, | ||
} from '@elastic/eui'; | ||
import { Workflow } from '../../../../common'; | ||
import { LaunchList } from './launch_list'; | ||
import { LaunchDetails } from './launch_details'; | ||
|
||
interface LaunchesProps { | ||
workflow?: Workflow; | ||
} | ||
|
||
/** | ||
* The launches page to browse launch history and view individual launch details. | ||
*/ | ||
export function Launches(props: LaunchesProps) { | ||
return ( | ||
<EuiPageContent> | ||
<EuiTitle> | ||
<h2>Launches</h2> | ||
</EuiTitle> | ||
<EuiSpacer size="m" /> | ||
<EuiFlexGroup direction="row"> | ||
<EuiFlexItem> | ||
<LaunchList /> | ||
</EuiFlexItem> | ||
<EuiFlexItem> | ||
<LaunchDetails /> | ||
</EuiFlexItem> | ||
</EuiFlexGroup> | ||
</EuiPageContent> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
export { Prototype } from './prototype'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have this setter as a prop since typically any filter changes will need to be propagated to the parent component(s).