Skip to content

Commit

Permalink
Updates for projects view filter toolbar
Browse files Browse the repository at this point in the history
  • Loading branch information
jeff-phillips-18 committed Sep 10, 2024
1 parent 8b1334a commit d934057
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class TableToolbar extends Contextual<HTMLElement> {
}

findFilterMenuOption(id: string, name: string): Cypress.Chainable<JQuery<HTMLElement>> {
return this.findToggleButton(id).parents().findByRole('option', { name });
return this.findToggleButton(id).parents().findByRole('menuitem', { name });
}

findSearchInput(): Cypress.Chainable<JQuery<HTMLElement>> {
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/__tests__/cypress/cypress/pages/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { DeleteModal } from '~/__tests__/cypress/cypress/pages/components/Delete
import { TableRow } from './components/table';
import { TableToolbar } from './components/TableToolbar';

class ProjectListToolbar extends TableToolbar {}
class ProjectListToolbar extends TableToolbar {
findFilterInput(name: string): Cypress.Chainable<JQuery<HTMLElement>> {
return this.find().findByLabelText(`Filter by ${name}`);
}
}

class NotebookRow extends TableRow {
findNotebookImageAvailability() {
return cy.findByTestId('notebook-image-availability');
Expand Down Expand Up @@ -89,7 +94,7 @@ class ProjectListPage {
}

getTableToolbar() {
return new ProjectListToolbar(() => cy.findByTestId('dashboard-table-toolbar'));
return new ProjectListToolbar(() => cy.findByTestId('projects-table-toolbar'));
}

findCreateWorkbenchButton() {
Expand All @@ -109,7 +114,7 @@ class ProjectListPage {

class CreateEditProjectModal extends Modal {
constructor(private edit = false) {
super(`${edit ? 'Edit' : 'Create'} data science project`);
super(`${edit ? 'Edit' : 'Create'} project`);
}

findNameInput() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ describe('Data science projects details', () => {

// Select the "Name" filter
const projectListToolbar = projectListPage.getTableToolbar();
projectListToolbar.findFilterMenuOption('filter-dropdown-select', 'Name').click();
projectListToolbar.findSearchInput().type('Test Project');
projectListToolbar.findFilterMenuOption('filter-toolbar-dropdown', 'Name').click();
projectListToolbar.findFilterInput('name').type('Test Project');
// Verify only rows with the typed run name exist
projectListPage.getProjectRow('Test Project').find().should('exist');
});
Expand All @@ -208,8 +208,8 @@ describe('Data science projects details', () => {

// Select the "User" filter
const projectListToolbar = projectListPage.getTableToolbar();
projectListToolbar.findFilterMenuOption('filter-dropdown-select', 'User').click();
projectListToolbar.findSearchInput().type('test-user');
projectListToolbar.findFilterMenuOption('filter-toolbar-dropdown', 'User').click();
projectListToolbar.findFilterInput('user').type('test-user');
// Verify only rows with the typed run user exist
projectListPage.getProjectRow('Test Project').find().should('exist');
});
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/components/PopoverListContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as React from 'react';
import {
Text,
TextContent,
TextContentProps,
TextList,
TextListItem,
} from '@patternfly/react-core';

type PopoverListContentProps = TextContentProps & {
leadText?: React.ReactNode;
listHeading?: React.ReactNode;
listItems: React.ReactNode[];
};

const ContentText: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<Text component="small" style={{ color: 'var(--Text---pf-v5-global--Color--100)' }}>
{children}
</Text>
);

const PopoverListContent: React.FC<PopoverListContentProps> = ({
leadText,
listHeading,
listItems,
...props
}) => (
<TextContent {...props}>
{leadText ? <ContentText>{leadText}</ContentText> : null}
{listHeading ? <Text component="h4">{listHeading}</Text> : null}
<TextList>
{listItems.map((item, index) => (
<TextListItem key={index}>
<ContentText>{item}</ContentText>
</TextListItem>
))}
</TextList>
</TextContent>
);

export default PopoverListContent;
21 changes: 6 additions & 15 deletions frontend/src/pages/projects/screens/projects/EmptyProjects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import {
Popover,
Button,
Icon,
TextContent,
TextList,
TextListItem,
} from '@patternfly/react-core';
import { useNavigate } from 'react-router-dom';
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import PopoverListContent from '~/components/PopoverListContent';
import projectsEmptyStateImg from '~/images/empty-state-projects-color.svg';
import { FindAdministratorOptions } from '~/pages/projects/screens/projects/const';
import NewProjectButton from './NewProjectButton';

type EmptyProjectsProps = {
Expand Down Expand Up @@ -54,18 +53,10 @@ const EmptyProjects: React.FC<EmptyProjectsProps> = ({ allowCreate }) => {
minWidth="400px"
headerContent="Your administrator might be:"
bodyContent={
<TextContent data-testid="projects-empty-admin-help-content">
<TextList>
<TextListItem>The person who gave you your username</TextListItem>
<TextListItem>
Someone in your IT department or Help desk (at a company or school)
</TextListItem>
<TextListItem>
The person who manages your email service or web site (in a small business or
club)
</TextListItem>
</TextList>
</TextContent>
<PopoverListContent
data-testid="projects-empty-admin-help-content"
listItems={FindAdministratorOptions}
/>
}
>
<Button data-testid="projects-empty-admin-help" isInline variant="link">
Expand Down
96 changes: 43 additions & 53 deletions frontend/src/pages/projects/screens/projects/ProjectListView.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import * as React from 'react';
import { Button, ToolbarGroup, ToolbarItem } from '@patternfly/react-core';
import { useNavigate } from 'react-router-dom';
import { Table } from '~/components/table';
import DashboardSearchField, { SearchType } from '~/concepts/dashboard/DashboardSearchField';
import { ProjectKind } from '~/k8sTypes';
import { getProjectOwner } from '~/concepts/projects/utils';
import { ProjectsContext } from '~/concepts/projects/ProjectsContext';
import ProjectTableRow from '~/pages/projects/screens/projects/ProjectTableRow';
import { getDisplayNameFromK8sResource } from '~/concepts/k8s/utils';
import NewProjectButton from './NewProjectButton';
import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView';
import ProjectsToolbar from '~/pages/projects/screens/projects/ProjectsToolbar';
import {
initialProjectsFilterData,
ProjectsFilterDataType,
} from '~/pages/projects/screens/projects/const';
import { columns, subColumns } from './tableData';
import DeleteProjectModal from './DeleteProjectModal';
import ManageProjectModal from './ManageProjectModal';
Expand All @@ -20,27 +23,41 @@ type ProjectListViewProps = {
const ProjectListView: React.FC<ProjectListViewProps> = ({ allowCreate }) => {
const { projects } = React.useContext(ProjectsContext);
const navigate = useNavigate();
const [searchType, setSearchType] = React.useState<SearchType>(SearchType.NAME);
const [search, setSearch] = React.useState('');
const filteredProjects = projects.filter((project) => {
if (!search) {
return true;
}
const [filterData, setFilterData] =
React.useState<ProjectsFilterDataType>(initialProjectsFilterData);
const onClearFilters = React.useCallback(
() => setFilterData(initialProjectsFilterData),
[setFilterData],
);

const filteredProjects = React.useMemo(
() =>
projects.filter((project) => {
const nameFilter = filterData.Name?.toLowerCase();
const userFilter = filterData.User?.toLowerCase();

switch (searchType) {
case SearchType.NAME:
return getDisplayNameFromK8sResource(project).toLowerCase().includes(search.toLowerCase());
case SearchType.USER:
return getProjectOwner(project).toLowerCase().includes(search.toLowerCase());
default:
return true;
}
});
if (
nameFilter &&
!getDisplayNameFromK8sResource(project).toLowerCase().includes(nameFilter)
) {
return false;
}

return !userFilter || getProjectOwner(project).toLowerCase().includes(userFilter);
}),
[projects, filterData],
);

const resetFilters = () => {
setSearch('');
setFilterData(initialProjectsFilterData);
};

const onFilterUpdate = React.useCallback(
(key: string, value: string | { label: string; value: string } | undefined) =>
setFilterData((prevValues) => ({ ...prevValues, [key]: value })),
[setFilterData],
);

const [deleteData, setDeleteData] = React.useState<ProjectKind | undefined>();
const [editData, setEditData] = React.useState<ProjectKind | undefined>();
const [refreshIds, setRefreshIds] = React.useState<string[]>([]);
Expand All @@ -55,14 +72,7 @@ const ProjectListView: React.FC<ProjectListViewProps> = ({ allowCreate }) => {
hasNestedHeader
columns={columns}
subColumns={subColumns}
emptyTableView={
<>
No projects match your filters.{' '}
<Button variant="link" isInline onClick={resetFilters}>
Clear filters
</Button>
</>
}
emptyTableView={<DashboardEmptyTableView onClearFilters={resetFilters} />}
data-testid="project-view-table"
rowRenderer={(project) => (
<ProjectTableRow
Expand All @@ -74,32 +84,12 @@ const ProjectListView: React.FC<ProjectListViewProps> = ({ allowCreate }) => {
/>
)}
toolbarContent={
<>
<ToolbarGroup>
<ToolbarItem>
<DashboardSearchField
types={[SearchType.NAME, SearchType.USER]}
searchType={searchType}
searchValue={search}
onSearchTypeChange={(newSearchType: SearchType) => {
setSearchType(newSearchType);
}}
onSearchValueChange={(searchValue: string) => {
setSearch(searchValue);
}}
/>
</ToolbarItem>
</ToolbarGroup>
<ToolbarGroup align={{ default: 'alignRight' }}>
{allowCreate && (
<ToolbarItem>
<NewProjectButton
onProjectCreated={(projectName) => navigate(`/projects/${projectName}`)}
/>
</ToolbarItem>
)}
</ToolbarGroup>
</>
<ProjectsToolbar
allowCreate={allowCreate}
filterData={filterData}
onFilterUpdate={onFilterUpdate}
onClearFilters={onClearFilters}
/>
}
/>
<ManageProjectModal
Expand Down
96 changes: 96 additions & 0 deletions frontend/src/pages/projects/screens/projects/ProjectsToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as React from 'react';
import {
Button,
Icon,
Popover,
SearchInput,
ToolbarGroup,
ToolbarItem,
} from '@patternfly/react-core';
import { useNavigate } from 'react-router-dom';
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import PopoverListContent from '~/components/PopoverListContent';
import FilterToolbar from '~/components/FilterToolbar';
import {
FindAdministratorOptions,
ProjectsFilterDataType,
projectsFilterOptions,
ProjectsFilterOptions,
} from '~/pages/projects/screens/projects/const';
import NewProjectButton from './NewProjectButton';

type ProjectsToolbarProps = {
allowCreate: boolean;
filterData: ProjectsFilterDataType;
onFilterUpdate: (key: string, value?: string | { label: string; value: string }) => void;
onClearFilters: () => void;
};

const ProjectsToolbar: React.FC<ProjectsToolbarProps> = ({
allowCreate,
filterData,
onFilterUpdate,
onClearFilters,
}) => {
const navigate = useNavigate();

return (
<FilterToolbar<keyof typeof projectsFilterOptions>
data-testid="projects-table-toolbar"
filterOptions={projectsFilterOptions}
filterOptionRenders={{
[ProjectsFilterOptions.name]: ({ onChange, ...props }) => (
<SearchInput
{...props}
aria-label="Filter by name"
placeholder="Filter by name"
onChange={(_event, value) => onChange(value)}
/>
),
[ProjectsFilterOptions.user]: ({ onChange, ...props }) => (
<SearchInput
{...props}
aria-label="Filter by user"
placeholder="Filter by user"
onChange={(_event, value) => onChange(value)}
/>
),
}}
filterData={filterData}
onClearFilters={onClearFilters}
onFilterUpdate={onFilterUpdate}
>
<ToolbarGroup>
<ToolbarItem>
{allowCreate ? (
<NewProjectButton
onProjectCreated={(projectName) => navigate(`/projects/${projectName}`)}
/>
) : (
<Popover
minWidth="400px"
headerContent="Need another project?"
bodyContent={
<PopoverListContent
data-testid="projects-admin-help-content"
leadText="To request a new project, contact your administrator."
listHeading="Your administrator might be:"
listItems={FindAdministratorOptions}
/>
}
>
<Button data-testid="projects-empty-admin-help" variant="link">
<Icon isInline aria-label="More info">
<OutlinedQuestionCircleIcon />
</Icon>
<span className="pf-v5-u-ml-xs">Need another project?</span>
</Button>
</Popover>
)}
</ToolbarItem>
</ToolbarGroup>
</FilterToolbar>
);
};

export default ProjectsToolbar;
22 changes: 22 additions & 0 deletions frontend/src/pages/projects/screens/projects/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export enum ProjectsFilterOptions {
name = 'Name',
user = 'User',
}

export const projectsFilterOptions = {
[ProjectsFilterOptions.name]: 'Name',
[ProjectsFilterOptions.user]: 'User',
};

export type ProjectsFilterDataType = Record<ProjectsFilterOptions, string | undefined>;

export const initialProjectsFilterData: ProjectsFilterDataType = {
[ProjectsFilterOptions.name]: '',
[ProjectsFilterOptions.user]: '',
};

export const FindAdministratorOptions = [
'The person who gave you your username',
'Someone in your IT department or Help desk (at a company or school)',
'The person who manages your email service or web site (in a small business or club)',
];

0 comments on commit d934057

Please sign in to comment.