Skip to content

Commit

Permalink
[EPM] Share package icon location hook (#62072) (#62983)
Browse files Browse the repository at this point in the history
* EPM detail page now uses same icon as list page.
* Moved hook from ./components to ./hooks
* Add optional `tryApi` param to `<PackageIcon>`

Trusts given values by default but can opt-in to side-effects like calling API. Prevents trying API when we know none are present (api explicitly returns none).

This also fixes the issue in master where the API was called several times for each item in the datasources list.
  • Loading branch information
John Schulz authored Apr 8, 2020
1 parent 0a7c421 commit 2ae38b7
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,78 +3,12 @@
* 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, { useEffect, useMemo, useState } from 'react';
import { ICON_TYPES, EuiIcon, EuiIconProps } from '@elastic/eui';
import { PackageInfo, PackageListItem } from '../../../../common/types/models';
import { useLinks } from '../sections/epm/hooks';
import { epmRouteService } from '../../../../common/services';
import { sendRequest } from '../hooks/use_request';
import { GetInfoResponse } from '../types';
type Package = PackageInfo | PackageListItem;
import React from 'react';
import { EuiIcon, EuiIconProps } from '@elastic/eui';
import { usePackageIconType, UsePackageIconType } from '../hooks';

const CACHED_ICONS = new Map<string, string>();

export const PackageIcon: React.FunctionComponent<{
packageName: string;
version?: string;
icons?: Package['icons'];
} & Omit<EuiIconProps, 'type'>> = ({ packageName, version, icons, ...euiIconProps }) => {
const iconType = usePackageIcon(packageName, version, icons);
export const PackageIcon: React.FunctionComponent<UsePackageIconType &
Omit<EuiIconProps, 'type'>> = ({ packageName, version, icons, tryApi, ...euiIconProps }) => {
const iconType = usePackageIconType({ packageName, version, icons, tryApi });
return <EuiIcon size="s" type={iconType} {...euiIconProps} />;
};

const usePackageIcon = (packageName: string, version?: string, icons?: Package['icons']) => {
const { toImage } = useLinks();
const [iconType, setIconType] = useState<string>(''); // FIXME: use `empty` icon during initialization - see: https://github.com/elastic/kibana/issues/60622
const pkgKey = `${packageName}-${version ?? ''}`;

// Generates an icon path or Eui Icon name based on an icon list from the package
// or by using the package name against logo icons from Eui
const fromInput = useMemo(() => {
return (iconList?: Package['icons']) => {
const svgIcons = iconList?.filter(iconDef => iconDef.type === 'image/svg+xml');
const localIconSrc = Array.isArray(svgIcons) && svgIcons[0]?.src;
if (localIconSrc) {
CACHED_ICONS.set(pkgKey, toImage(localIconSrc));
setIconType(CACHED_ICONS.get(pkgKey) as string);
return;
}

const euiLogoIcon = ICON_TYPES.find(key => key.toLowerCase() === `logo${packageName}`);
if (euiLogoIcon) {
CACHED_ICONS.set(pkgKey, euiLogoIcon);
setIconType(euiLogoIcon);
return;
}

CACHED_ICONS.set(pkgKey, 'package');
setIconType('package');
};
}, [packageName, pkgKey, toImage]);

useEffect(() => {
if (CACHED_ICONS.has(pkgKey)) {
setIconType(CACHED_ICONS.get(pkgKey) as string);
return;
}

// Use API to see if package has icons defined
if (!icons && version) {
fromPackageInfo(pkgKey)
.catch(() => undefined) // ignore API errors
.then(fromInput);
} else {
fromInput(icons);
}
}, [icons, toImage, packageName, version, fromInput, pkgKey]);

return iconType;
};

const fromPackageInfo = async (pkgKey: string) => {
const { data } = await sendRequest<GetInfoResponse>({
path: epmRouteService.getInfoPath(pkgKey),
method: 'get',
});
return data?.response?.icons;
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { useCore, CoreContext } from './use_core';
export { useConfig, ConfigContext } from './use_config';
export { useSetupDeps, useStartDeps, DepsContext } from './use_deps';
export { useLink } from './use_link';
export { usePackageIconType, UsePackageIconType } from './use_package_icon_type';
export { usePagination, Pagination } from './use_pagination';
export { useDebounce } from './use_debounce';
export * from './use_request';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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 { useEffect, useState } from 'react';
import { ICON_TYPES } from '@elastic/eui';
import { PackageInfo, PackageListItem } from '../../../../common/types/models';
import { useLinks } from '../sections/epm/hooks';
import { sendGetPackageInfoByKey } from './index';

type Package = PackageInfo | PackageListItem;

export interface UsePackageIconType {
packageName: Package['name'];
version: Package['version'];
icons?: Package['icons'];
tryApi?: boolean; // should it call API to try to find missing icons?
}

const CACHED_ICONS = new Map<string, string>();

export const usePackageIconType = ({
packageName,
version,
icons: paramIcons,
tryApi = false,
}: UsePackageIconType) => {
const { toImage } = useLinks();
const [iconList, setIconList] = useState<UsePackageIconType['icons']>();
const [iconType, setIconType] = useState<string>(''); // FIXME: use `empty` icon during initialization - see: https://github.com/elastic/kibana/issues/60622
const pkgKey = `${packageName}-${version}`;

// Generates an icon path or Eui Icon name based on an icon list from the package
// or by using the package name against logo icons from Eui
useEffect(() => {
if (CACHED_ICONS.has(pkgKey)) {
setIconType(CACHED_ICONS.get(pkgKey) || '');
return;
}
const svgIcons = (paramIcons || iconList)?.filter(iconDef => iconDef.type === 'image/svg+xml');
const localIconSrc = Array.isArray(svgIcons) && svgIcons[0]?.src;
if (localIconSrc) {
CACHED_ICONS.set(pkgKey, toImage(localIconSrc));
setIconType(CACHED_ICONS.get(pkgKey) || '');
return;
}

const euiLogoIcon = ICON_TYPES.find(key => key.toLowerCase() === `logo${packageName}`);
if (euiLogoIcon) {
CACHED_ICONS.set(pkgKey, euiLogoIcon);
setIconType(euiLogoIcon);
return;
}

if (tryApi && !paramIcons && !iconList) {
sendGetPackageInfoByKey(pkgKey)
.catch(error => undefined) // Ignore API errors
.then(res => {
CACHED_ICONS.delete(pkgKey);
setIconList(res?.data?.response?.icons);
});
}

CACHED_ICONS.set(pkgKey, 'package');
setIconType('package');
}, [paramIcons, pkgKey, toImage, iconList, packageName, iconType, tryApi]);

return iconType;
};
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,15 @@ export const StepSelectPackage: React.FunctionComponent<{
return {
label: title || name,
key: pkgkey,
prepend: <PackageIcon packageName={name} version={version} icons={icons} size="m" />,
prepend: (
<PackageIcon
packageName={name}
version={version}
icons={icons}
size="m"
tryApi={true}
/>
),
checked: selectedPkgKey === pkgkey ? 'on' : undefined,
};
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export const DatasourcesTable: React.FunctionComponent<Props> = ({
packageName={datasource.package.name}
version={datasource.package.version}
size="m"
tryApi={true}
/>
</EuiFlexItem>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export function IconPanel({ iconType }: { iconType: IconType }) {
text-align: center;
vertical-align: middle;
padding: ${props => props.theme.eui.spacerSizes.xl};
svg {
svg,
img {
height: ${props => props.theme.eui.euiKeyPadMenuSize};
width: ${props => props.theme.eui.euiKeyPadMenuSize};
}
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 { EuiPage, EuiPageBody, EuiPageProps, ICON_TYPES } from '@elastic/eui';
import { EuiPage, EuiPageBody, EuiPageProps } from '@elastic/eui';
import React, { Fragment, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import styled from 'styled-components';
Expand All @@ -12,7 +12,7 @@ import { PackageInfo } from '../../../../types';
import { useSetPackageInstallStatus } from '../../hooks';
import { Content } from './content';
import { Header } from './header';
import { sendGetPackageInfoByKey } from '../../../../hooks';
import { sendGetPackageInfoByKey, usePackageIconType } from '../../../../hooks';

export const DEFAULT_PANEL: DetailViewPanelName = 'overview';

Expand Down Expand Up @@ -62,8 +62,8 @@ const FullWidthContent = styled(EuiPage)`

type LayoutProps = PackageInfo & Pick<DetailParams, 'panel'> & Pick<EuiPageProps, 'restrictWidth'>;
export function DetailLayout(props: LayoutProps) {
const { name, restrictWidth } = props;
const iconType = ICON_TYPES.find(key => key.toLowerCase() === `logo${name}`);
const { name: packageName, version, icons, restrictWidth } = props;
const iconType = usePackageIconType({ packageName, version, icons });

return (
<Fragment>
Expand Down

0 comments on commit 2ae38b7

Please sign in to comment.