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

Add multi-filtering to tables; refactor details pages into tabs #63

Merged
merged 4 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions common/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,29 @@ export type Workflow = {
workspaceFlowState?: WorkspaceFlowState;
template: UseCaseTemplate;
lastUpdated: number;
state: WORKFLOW_STATE;
};

export enum USE_CASE {
SEMANTIC_SEARCH = 'semantic_search',
CUSTOM = 'custom',
}

/**
********** MISC TYPES/INTERFACES ************
*/

// TODO: finalize how we have the launch data model
export type WorkflowLaunch = {
id: string;
state: WORKFLOW_STATE;
lastUpdated: number;
};

// TODO: finalize list of possible workflow states from backend
export enum WORKFLOW_STATE {
SUCCEEDED = 'Succeeded',
FAILED = 'Failed',
IN_PROGRESS = 'In progress',
NOT_STARTED = 'Not started',
}
5 changes: 5 additions & 0 deletions public/general_components/general-component-styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.multi-select-filter {
&--width {
width: 150px;
}
}
6 changes: 6 additions & 0 deletions public/general_components/index.ts
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';
98 changes: 98 additions & 0 deletions public/general_components/multi_select_filter.tsx
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;
Copy link
Member Author

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).

}

/**
* 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
Copy link
Member Author

Choose a reason for hiding this comment

The 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>
);
}
4 changes: 3 additions & 1 deletion public/pages/workflow_detail/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { saveWorkflow } from '../utils';
import { rfContext, AppState, removeDirty } from '../../../store';

interface WorkflowDetailHeaderProps {
tabs: any[];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tab isn't exposed by EUI/OUI library so we set as any.

workflow?: Workflow;
}

Expand All @@ -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
Expand All @@ -39,6 +39,8 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
Save
</EuiButton>,
]}
tabs={props.tabs}
bottomBorder={true}
/>
);
}
22 changes: 22 additions & 0 deletions public/pages/workflow_detail/launches/columns.tsx
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,
},
];
6 changes: 6 additions & 0 deletions public/pages/workflow_detail/launches/index.ts
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';
13 changes: 13 additions & 0 deletions public/pages/workflow_detail/launches/launch_details.tsx
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>;
}
119 changes: 119 additions & 0 deletions public/pages/workflow_detail/launches/launch_list.tsx
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())
);
}
42 changes: 42 additions & 0 deletions public/pages/workflow_detail/launches/launches.tsx
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>
);
}
6 changes: 6 additions & 0 deletions public/pages/workflow_detail/prototype/index.ts
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';
Loading
Loading