mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
import React, { useState } from "react";
|
|
import { useQuery } from "react-query";
|
|
import classnames from "classnames";
|
|
import { SOFTWARE_ICON_SIZES, SoftwareIconSizes } from "styles/var/icon_sizes";
|
|
import { AxiosError } from "axios";
|
|
import softwareAPI from "services/entities/software";
|
|
import { getMatchedSoftwareIcon } from "../";
|
|
|
|
const baseClass = "software-icon";
|
|
|
|
interface ISoftwareIconProps {
|
|
/** The software's name (used for fallback icon matching) */
|
|
name?: string;
|
|
/** The software's source (used for fallback icon matching) */
|
|
source?: string;
|
|
/** The icon size (default: 'small' 24x24 px) */
|
|
size?: SoftwareIconSizes;
|
|
/** The image URL or API path to fetch custom icon blob */
|
|
url?: string | null;
|
|
/** Timestamp string of when the icon was last uploaded
|
|
* (used to refetch stale icon if it was updated) */
|
|
uploadedAt?: string;
|
|
}
|
|
|
|
const SoftwareIcon = ({
|
|
name = "",
|
|
source = "",
|
|
size = "small",
|
|
url,
|
|
uploadedAt,
|
|
}: ISoftwareIconProps) => {
|
|
const classNames = classnames(baseClass, `${baseClass}__${size}`);
|
|
|
|
const [imgFailed, setImgFailed] = useState(false);
|
|
|
|
const isApiUrl =
|
|
(typeof url === "string" && url?.startsWith("/api/")) || false;
|
|
|
|
const { data: currentCustomIconBlob, isLoading } = useQuery<
|
|
Blob | undefined,
|
|
AxiosError,
|
|
string
|
|
>(
|
|
["softwareIcon", url, uploadedAt],
|
|
() => softwareAPI.getSoftwareIconFromApiUrl(url as string),
|
|
{
|
|
enabled: isApiUrl,
|
|
retry: false,
|
|
select: (blob) => (blob ? URL.createObjectURL(blob) : ""),
|
|
}
|
|
);
|
|
|
|
const imgClasses = classnames(
|
|
`${baseClass}__software-img`,
|
|
`${baseClass}__software-img-${size}`
|
|
);
|
|
|
|
let iconSrc: string | null = null;
|
|
|
|
if (isApiUrl) {
|
|
if (isLoading) {
|
|
// Return empty div while loading custom icon so component size doesn't jump
|
|
return <div className={classNames.concat(" loading-placeholder")} />;
|
|
}
|
|
if (currentCustomIconBlob) {
|
|
// Uses custom icon blob from API if fetch succeeded
|
|
iconSrc = currentCustomIconBlob;
|
|
}
|
|
} else if (url) {
|
|
// Use direct image URL (e.g. VPP image URL))
|
|
iconSrc = url;
|
|
}
|
|
|
|
// Only use direct image URL if it hasn't failed, if it has, skip and go to matched icons
|
|
if (iconSrc && !imgFailed) {
|
|
return (
|
|
<div className={classNames}>
|
|
<img
|
|
className={imgClasses}
|
|
src={iconSrc}
|
|
alt=""
|
|
onError={() => setImgFailed(true)} // e.g. 429 rate limit
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Fallback: Render a matched SVG icon by software name/source
|
|
const MatchedIcon = getMatchedSoftwareIcon({ name, source });
|
|
return (
|
|
<MatchedIcon
|
|
width={SOFTWARE_ICON_SIZES[size]}
|
|
height={SOFTWARE_ICON_SIZES[size]}
|
|
viewBox="0 0 32 32"
|
|
className={classNames}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default SoftwareIcon;
|