Skip to content

Commit

Permalink
[EPM] Packages list tabs (#60167)
Browse files Browse the repository at this point in the history
* Memo'ize some layout and EPM header components

* Split EPM home page into two tabs

* Clean up dead files and import paths

* Add empty state

* Use react routing for rendering tab content

* Fix import paths (again)
  • Loading branch information
jen-huang committed Mar 17, 2020
1 parent 1c968e1 commit 69ca01d
Show file tree
Hide file tree
Showing 63 changed files with 313 additions and 248 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/ingest_manager/common/types/models/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { SavedObjectAttributes } from '../../../../../../src/core/public';
import { SavedObjectAttributes } from 'src/core/public';
import { AGENT_TYPE_EPHEMERAL, AGENT_TYPE_PERMANENT, AGENT_TYPE_TEMPORARY } from '../../constants';

export type AgentType =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { SavedObjectAttributes } from '../../../../../../src/core/public';
import { SavedObjectAttributes } from 'src/core/public';
import {
Datasource,
DatasourcePackage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectAttributes } from '../../../../../../src/core/public';
import { SavedObjectAttributes } from 'src/core/public';

export interface EnrollmentAPIKey {
id: string;
Expand Down
6 changes: 1 addition & 5 deletions x-pack/plugins/ingest_manager/common/types/models/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@

// Follow pattern from https://github.com/elastic/kibana/pull/52447
// TODO: Update when https://github.com/elastic/kibana/issues/53021 is closed
import {
SavedObject,
SavedObjectAttributes,
SavedObjectReference,
} from '../../../../../../src/core/public';
import { SavedObject, SavedObjectAttributes, SavedObjectReference } from 'src/core/public';

export enum InstallationStatus {
installed = 'installed',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { memo } from 'react';
import styled from 'styled-components';
import { EuiFlexGroup, EuiFlexItem, EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui';
import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab';
Expand Down Expand Up @@ -35,13 +35,17 @@ export interface HeaderProps {
tabs?: EuiTabProps[];
}

const HeaderColumns: React.FC<Omit<HeaderProps, 'tabs'>> = memo(({ leftColumn, rightColumn }) => (
<EuiFlexGroup alignItems="center">
{leftColumn ? <EuiFlexItem>{leftColumn}</EuiFlexItem> : null}
{rightColumn ? <EuiFlexItem>{rightColumn}</EuiFlexItem> : null}
</EuiFlexGroup>
));

export const Header: React.FC<HeaderProps> = ({ leftColumn, rightColumn, tabs }) => (
<Container>
<Wrapper>
<EuiFlexGroup alignItems="center">
{leftColumn ? <EuiFlexItem>{leftColumn}</EuiFlexItem> : null}
{rightColumn ? <EuiFlexItem>{rightColumn}</EuiFlexItem> : null}
</EuiFlexGroup>
<HeaderColumns leftColumn={leftColumn} rightColumn={rightColumn} />
<EuiFlexGroup>
{tabs ? (
<EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export { PLUGIN_ID, EPM_API_ROUTES, AGENT_CONFIG_SAVED_OBJECT_TYPE } from '../..

export const BASE_PATH = '/app/ingestManager';
export const EPM_PATH = '/epm';
export const EPM_LIST_ALL_PACKAGES_PATH = EPM_PATH;
export const EPM_LIST_INSTALLED_PACKAGES_PATH = `${EPM_PATH}/installed`;
export const EPM_DETAIL_VIEW_PATH = `${EPM_PATH}/detail/:pkgkey/:panel?`;
export const AGENT_CONFIG_PATH = '/configs';
export const AGENT_CONFIG_DETAILS_PATH = `${AGENT_CONFIG_PATH}/`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { ChromeBreadcrumb } from '../../../../../../../../../src/core/public';
import { useCore } from '../../../hooks';
import { ChromeBreadcrumb } from 'src/core/public';
import { useCore } from './use_core';

export function useBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]) {
const { chrome } = useCore();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import React, { useContext } from 'react';
import { CoreStart } from 'kibana/public';
import { CoreStart } from 'src/core/public';

export const CoreContext = React.createContext<CoreStart | null>(null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpFetchQuery } from 'kibana/public';
import { HttpFetchQuery } from 'src/core/public';
import { useRequest, sendRequest } from './use_request';
import { agentConfigRouteService } from '../../services';
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { HttpFetchQuery } from 'kibana/public';
import { HttpFetchQuery } from 'src/core/public';
import { useRequest, sendRequest } from './use_request';
import { epmRouteService } from '../../services';
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpSetup } from 'kibana/public';
import { HttpSetup } from 'src/core/public';
import {
SendRequestConfig,
SendRequestResponse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useObservable } from 'react-use';
import { HashRouter as Router, Redirect, Switch, Route, RouteProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiErrorBoundary } from '@elastic/eui';
import { CoreStart, AppMountParameters } from 'kibana/public';
import { CoreStart, AppMountParameters } from 'src/core/public';
import { EuiThemeProvider } from '../../../../../legacy/common/eui_styled_components';
import {
IngestManagerSetupDeps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,76 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
import React, { Fragment, ReactNode } from 'react';
import React, { Fragment, ReactNode, useState } from 'react';
import {
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTitle,
// @ts-ignore
EuiSearchBar,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { Loading } from '../../../components';
import { PackageList } from '../../../types';
import { useLocalSearch, searchIdField } from '../hooks';
import { BadgeProps, PackageCard } from './package_card';

type ListProps = {
isLoading?: boolean;
controls?: ReactNode;
title: string;
list: PackageList;
} & BadgeProps;

export function PackageListGrid({ controls, title, list, showInstalledBadge }: ListProps) {
export function PackageListGrid({
isLoading,
controls,
title,
list,
showInstalledBadge,
}: ListProps) {
const [searchTerm, setSearchTerm] = useState('');
const localSearchRef = useLocalSearch(list);

const controlsContent = <ControlsColumn title={title} controls={controls} />;
const gridContent = <GridColumn list={list} showInstalledBadge={showInstalledBadge} />;
let gridContent: JSX.Element;

if (isLoading || !localSearchRef.current) {
gridContent = <Loading />;
} else {
const filteredList = searchTerm
? list.filter(item =>
(localSearchRef.current!.search(searchTerm) as PackageList)
.map(match => match[searchIdField])
.includes(item[searchIdField])
)
: list;
gridContent = <GridColumn list={filteredList} showInstalledBadge={showInstalledBadge} />;
}

return (
<EuiFlexGroup>
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem grow={1}>{controlsContent}</EuiFlexItem>
<EuiFlexItem grow={3}>{gridContent}</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiSearchBar
query={searchTerm}
box={{
placeholder: i18n.translate('xpack.ingestManager.epmList.searchPackagesPlaceholder', {
defaultMessage: 'Search for a package',
}),
incremental: true,
}}
onChange={({ queryText: userInput }: { queryText: string }) => {
setSearchTerm(userInput);
}}
/>
<EuiSpacer />
{gridContent}
</EuiFlexItem>
</EuiFlexGroup>
);
}
Expand All @@ -34,9 +85,9 @@ interface ControlsColumnProps {
function ControlsColumn({ controls, title }: ControlsColumnProps) {
return (
<Fragment>
<EuiText>
<EuiTitle size="s">
<h2>{title}</h2>
</EuiText>
</EuiTitle>
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem grow={2}>{controls}</EuiFlexItem>
Expand All @@ -53,11 +104,24 @@ type GridColumnProps = {
function GridColumn({ list }: GridColumnProps) {
return (
<EuiFlexGrid gutterSize="l" columns={3}>
{list.map(item => (
<EuiFlexItem key={`${item.name}-${item.version}`}>
<PackageCard {...item} />
{list.length ? (
list.map(item => (
<EuiFlexItem key={`${item.name}-${item.version}`}>
<PackageCard {...item} />
</EuiFlexItem>
))
) : (
<EuiFlexItem>
<EuiText>
<p>
<FormattedMessage
id="xpack.ingestManager.epmList.noPackagesFoundPlaceholder"
defaultMessage="No packages found"
/>
</p>
</EuiText>
</EuiFlexItem>
))}
)}
</EuiFlexGrid>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

// export { useBreadcrumbs } from './use_breadcrumbs';
export { useLinks } from './use_links';
export { useLocalSearch, searchIdField } from './use_local_search';
export {
PackageInstallProvider,
useDeletePackage,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Search as LocalSearch } from 'js-search';
import { useEffect, useRef } from 'react';
import { PackageList, PackageListItem } from '../../../types';

export type SearchField = keyof PackageListItem;
export const searchIdField: SearchField = 'name';
export const fieldsToSearch: SearchField[] = ['description', 'name', 'title'];

export function useLocalSearch(packageList: PackageList) {
const localSearchRef = useRef<LocalSearch | null>(null);

useEffect(() => {
if (!packageList.length) return;

const localSearch = new LocalSearch(searchIdField);
fieldsToSearch.forEach(field => localSearch.addIndex(field));
localSearch.addDocuments(packageList);
localSearchRef.current = localSearch;
}, [packageList]);

return localSearchRef;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React from 'react';
import { HashRouter as Router, Switch, Route } from 'react-router-dom';
import { useConfig } from '../../hooks';
import { CreateDatasourcePage } from '../agent_config/create_datasource_page';
import { Home } from './screens/home';
import { EPMHomePage } from './screens/home';
import { Detail } from './screens/detail';

export const EPMApp: React.FunctionComponent = () => {
Expand All @@ -23,8 +23,8 @@ export const EPMApp: React.FunctionComponent = () => {
<Route path="/epm/detail/:pkgkey/:panel?">
<Detail />
</Route>
<Route path="/epm/">
<Home />
<Route path="/epm/:tabId?" exact={true}>
<EPMHomePage />
</Route>
</Switch>
</Router>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ export function Header(props: HeaderProps) {
const { iconType, name, title, version } = props;
const hasWriteCapabilites = useCapabilities().write;
const { toListView } = useLinks();
// useBreadcrumbs([{ text: PLUGIN.TITLE, href: toListView() }, { text: title }]);

const ADD_DATASOURCE_URI = useLink(`${EPM_PATH}/${name}-${version}/add-datasource`);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,37 @@
*/
import { EuiFacetButton, EuiFacetGroup } from '@elastic/eui';
import React from 'react';
import { Loading } from '../../../../components';
import { CategorySummaryItem, CategorySummaryList } from '../../../../types';

export function CategoryFacets({
isLoading,
categories,
selectedCategory,
onCategoryChange,
}: {
isLoading?: boolean;
categories: CategorySummaryList;
selectedCategory: string;
onCategoryChange: (category: CategorySummaryItem) => unknown;
}) {
const controls = (
<EuiFacetGroup>
{categories.map(category => (
<EuiFacetButton
isSelected={category.id === selectedCategory}
key={category.id}
id={category.id}
quantity={category.count}
onClick={() => onCategoryChange(category)}
>
{category.title}
</EuiFacetButton>
))}
{isLoading ? (
<Loading />
) : (
categories.map(category => (
<EuiFacetButton
isSelected={category.id === selectedCategory}
key={category.id}
id={category.id}
quantity={category.count}
onClick={() => onCategoryChange(category)}
>
{category.title}
</EuiFacetButton>
))
)}
</EuiFacetGroup>
);

Expand Down
Loading

0 comments on commit 69ca01d

Please sign in to comment.