Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { IconType } from '@elastic/eui';
import { type IconType } from '@elastic/eui';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { getStackConnectorLogo } from '@kbn/stack-connectors-plugin/public/common/logos';
import { ElasticsearchLogo } from './icons/elasticsearch.svg';
import { HARDCODED_ICONS } from './icons/hardcoded_icons';
import { KibanaLogo } from './icons/kibana.svg';
Expand All @@ -27,62 +26,78 @@ const DEFAULT_CONNECTOR_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16
<circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="2"/>
<circle cx="8" cy="8" r="2" fill="currentColor"/>
</svg>`;
const DEFAULT_CONNECTOR_DATA_URL = `data:image/svg+xml;base64,${btoa(DEFAULT_CONNECTOR_SVG)}`;

type LazyImageComponent = React.LazyExoticComponent<
React.ComponentType<{ width: number; height: number }>
> & {
_payload: {
_result: () => Promise<{ default: React.ComponentType<{ width: number; height: number }> }>;
};
};

/**
* Type guard to check if a component is a LazyExoticComponent
* LazyExoticComponent has an internal _payload property with a _load function
*/
function isLazyExoticComponent(component: unknown): component is LazyImageComponent {
const comp = component as unknown as LazyImageComponent;
return typeof comp?._payload?._result === 'function';
}

/**
* Resolve a LazyExoticComponent to its actual component
* Accesses React's internal _payload._result API to resolve the lazy component
*/
async function resolveLazyComponent(
lazyComponent: LazyImageComponent
): Promise<React.ComponentType<{ width: number; height: number }>> {
// Access React's internal payload to get the loader function
const module = await lazyComponent._payload._result();
// Return the default export (the actual component)
return module.default;
}

/**
* Get data URL for a connector icon (supports SVG, PNG, and other image formats)
* Returns a full data URL (e.g., "data:image/svg+xml;base64,..." or "data:image/png;base64,...")
*/
export async function getStepIconBase64(connector: GetStepIconBase64Params): Promise<string> {
try {
// Only use connector.icon if it's already a data URL
if (
connector.icon &&
typeof connector.icon === 'string' &&
connector.icon.startsWith('data:')
) {
return connector.icon;
// The icon from action registry,
if (connector.icon) {
// data URL strings or lazy components supported.
// built-in EUI icons are not supported (e.g. 'logoSlack', 'inference') use hardcoded icons for them instead.
if (typeof connector.icon === 'string' && connector.icon.startsWith('data:')) {
return connector.icon;
}
if (isLazyExoticComponent(connector.icon)) {
const IconComponent = await resolveLazyComponent(connector.icon);
return getDataUrlFromReactComponent(IconComponent);
}
}

const connectorType = connector.actionTypeId;
if (connectorType === 'elasticsearch') {
if (connector.actionTypeId === 'elasticsearch') {
return getDataUrlFromReactComponent(ElasticsearchLogo);
}

if (connectorType === 'kibana') {
if (connector.actionTypeId === 'kibana') {
return getDataUrlFromReactComponent(KibanaLogo);
}

// Handle connectors that use EUI built-in icons instead of custom logo components
if (connectorType === 'slack' || connectorType === 'slack_api') {
// hardcoded slack logo - convert to data URL if it's just base64
const slackIcon = HARDCODED_ICONS.slack;
if (slackIcon.startsWith('data:')) {
return slackIcon;
}
return `data:image/svg+xml;base64,${slackIcon}`;
}

if (connectorType in HARDCODED_ICONS) {
const hardcodedIcon = HARDCODED_ICONS[connectorType as keyof typeof HARDCODED_ICONS];
const hardcodedIcon = HARDCODED_ICONS[connector.actionTypeId];
if (hardcodedIcon) {
if (hardcodedIcon.startsWith('data:')) {
return hardcodedIcon;
}
return `data:image/svg+xml;base64,${hardcodedIcon}`;
}

const dotConnectorType = `.${connectorType}`;
// First, try to get the logo directly from stack connectors
const LogoComponent = await getStackConnectorLogo(dotConnectorType);
if (LogoComponent) {
// LogoComponent is a React component, not an IconType, so handle it separately
return getDataUrlFromReactComponent(LogoComponent);
}

// Fallback to default icon for other connector types
return `data:image/svg+xml;base64,${btoa(DEFAULT_CONNECTOR_SVG)}`;
return DEFAULT_CONNECTOR_DATA_URL;
} catch (error) {
// Fallback to default static icon
return `data:image/svg+xml;base64,${btoa(DEFAULT_CONNECTOR_SVG)}`;
return DEFAULT_CONNECTOR_DATA_URL;
}
}

Expand Down Expand Up @@ -122,8 +137,7 @@ function getDataUrlFromReactComponent(
if (hasFillNone) {
// Remove fill="none" and add currentColor fill
htmlString = htmlString
.replace(/fill="none"/gi, '')
.replace(/fill='none'/gi, '')
.replaceAll(/fill="none"/gi, '')
.replace(/<svg([^>]*?)>/, '<svg$1 fill="currentColor">');
}
}
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ import sparkles from './sparkles.svg';
import user from './user.svg';
import warning from './warning.svg';

export const HARDCODED_ICONS = {
export const HARDCODED_ICONS: Record<string, string> = {
'.slack': slackLogoSvg,
'.slack_api': slackLogoSvg,
'.email': email,
'.inference': sparkles,
elasticsearch: elasticsearchLogoSvg,
kibana: kibanaLogoSvg,
slack: slackLogoSvg,
email,
inference: sparkles,
console,
http: globe,
foreach: refresh,
Expand Down
Loading