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
2 changes: 1 addition & 1 deletion workspaces/frontend/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"eslint.options": {
"rulePaths": ["./eslint-local-rules"]
}
}
}
34 changes: 20 additions & 14 deletions workspaces/frontend/src/app/components/WorkspaceTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
PaginationVariant,
Pagination,
Content,
Brand,
Tooltip,
Bullseye,
Button,
Expand All @@ -28,7 +27,6 @@ import {
ExclamationTriangleIcon,
TimesCircleIcon,
QuestionCircleIcon,
CodeIcon,
} from '@patternfly/react-icons';
import { formatDistanceToNow } from 'date-fns';
import { Workspace, WorkspaceState } from '~/shared/api/backendApiTypes';
Expand All @@ -48,10 +46,12 @@ import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds';
import { WorkspaceConnectAction } from '~/app/pages/Workspaces/WorkspaceConnectAction';
import CustomEmptyState from '~/shared/components/CustomEmptyState';
import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter';
import WithValidImage from '~/shared/components/WithValidImage';
import {
formatResourceFromWorkspace,
formatWorkspaceIdleState,
} from '~/shared/utilities/WorkspaceUtils';
import ImageFallback from '~/shared/components/ImageFallback';

const {
fields: wsTableColumns,
Expand Down Expand Up @@ -436,19 +436,25 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
case 'kind':
return (
<Td key={columnKey} dataLabel={wsTableColumns[columnKey].label}>
{kindLogoDict[workspace.workspaceKind.name] ? (
<Tooltip content={workspace.workspaceKind.name}>
<Brand
src={kindLogoDict[workspace.workspaceKind.name]}
alt={workspace.workspaceKind.name}
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
<WithValidImage
imageSrc={kindLogoDict[workspace.workspaceKind.name]}
skeletonWidth="20px"
fallback={
<ImageFallback
imageSrc={kindLogoDict[workspace.workspaceKind.name]}
/>
</Tooltip>
) : (
<Tooltip content={workspace.workspaceKind.name}>
<CodeIcon />
</Tooltip>
)}
}
>
{(validSrc) => (
<Tooltip content={workspace.workspaceKind.name}>
<img
src={validSrc}
alt={workspace.workspaceKind.name}
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
/>
</Tooltip>
)}
</WithValidImage>
</Td>
);
case 'namespace':
Expand Down
27 changes: 16 additions & 11 deletions workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
DrawerContentBody,
PageSection,
Content,
Brand,
Tooltip,
Label,
Toolbar,
Expand Down Expand Up @@ -34,13 +33,15 @@ import {
ActionsColumn,
IActions,
} from '@patternfly/react-table';
import { CodeIcon, FilterIcon } from '@patternfly/react-icons';
import { FilterIcon } from '@patternfly/react-icons';
import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds';
import { useWorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind';
import { WorkspaceKindsColumns } from '~/app/types';
import ThemeAwareSearchInput from '~/app/components/ThemeAwareSearchInput';
import CustomEmptyState from '~/shared/components/CustomEmptyState';
import WithValidImage from '~/shared/components/WithValidImage';
import ImageFallback from '~/shared/components/ImageFallback';
import { useTypedNavigate } from '~/app/routerHelper';
import { WorkspaceKindDetails } from './details/WorkspaceKindDetails';

Expand Down Expand Up @@ -555,15 +556,19 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
<Tbody id="workspace-kind-table-content" key={rowIndex} data-testid="table-body">
<Tr id={`workspace-kind-table-row-${rowIndex + 1}`}>
<Td dataLabel={columns.icon.name} style={{ width: '50px' }}>
{workspaceKind.icon.url ? (
<Brand
src={workspaceKind.icon.url}
alt={workspaceKind.name}
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
/>
) : (
<CodeIcon />
)}
<WithValidImage
imageSrc={workspaceKind.icon.url}
skeletonWidth="20px"
fallback={<ImageFallback imageSrc={workspaceKind.icon.url} />}
>
{(validSrc) => (
<img
src={validSrc}
alt={workspaceKind.name}
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
/>
)}
</WithValidImage>
</Td>
<Td dataLabel={columns.name.name}>{workspaceKind.name}</Td>
<Td
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
DescriptionListGroup,
DescriptionListDescription,
Divider,
Brand,
} from '@patternfly/react-core';
import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import ImageFallback from '~/shared/components/ImageFallback';
import WithValidImage from '~/shared/components/WithValidImage';

type WorkspaceDetailsOverviewProps = {
workspaceKind: WorkspaceKind;
Expand Down Expand Up @@ -48,7 +49,19 @@ export const WorkspaceKindDetailsOverview: React.FunctionComponent<
<DescriptionListGroup>
<DescriptionListTerm style={{ alignSelf: 'center' }}>Icon</DescriptionListTerm>
<DescriptionListDescription>
<Brand src={workspaceKind.icon.url} alt={workspaceKind.name} style={{ width: '40px' }} />
<WithValidImage
imageSrc={workspaceKind.icon.url}
skeletonWidth="40px"
fallback={
<ImageFallback
imageSrc={workspaceKind.icon.url}
extended
message="Cannot load icon image"
/>
}
>
{(validSrc) => <img src={validSrc} alt={workspaceKind.name} style={{ width: '40px' }} />}
</WithValidImage>
</DescriptionListDescription>
<DescriptionListTerm style={{ alignSelf: 'center' }}>Icon URL</DescriptionListTerm>
<DescriptionListDescription>
Expand All @@ -61,7 +74,19 @@ export const WorkspaceKindDetailsOverview: React.FunctionComponent<
<DescriptionListGroup>
<DescriptionListTerm style={{ alignSelf: 'center' }}>Logo</DescriptionListTerm>
<DescriptionListDescription>
<Brand src={workspaceKind.logo.url} alt={workspaceKind.name} style={{ width: '40px' }} />
<WithValidImage
imageSrc={workspaceKind.logo.url}
skeletonWidth="40px"
fallback={
<ImageFallback
imageSrc={workspaceKind.logo.url}
extended
message="Cannot load logo image"
/>
}
>
{(validSrc) => <img src={validSrc} alt={workspaceKind.name} style={{ width: '40px' }} />}
</WithValidImage>
</DescriptionListDescription>
<DescriptionListTerm style={{ alignSelf: 'center' }}>Logo URL</DescriptionListTerm>
<DescriptionListDescription>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
import { WorkspaceKind } from '~/shared/api/backendApiTypes';
import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter';
import CustomEmptyState from '~/shared/components/CustomEmptyState';
import ImageFallback from '~/shared/components/ImageFallback';
import WithValidImage from '~/shared/components/WithValidImage';
import { defineDataFields, FilterableDataFieldKey } from '~/app/filterableDataHelper';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down Expand Up @@ -111,7 +113,21 @@ export const WorkspaceFormKindList: React.FunctionComponent<WorkspaceFormKindLis
onChange,
}}
>
<img src={kind.logo.url} alt={`${kind.name} logo`} style={{ maxWidth: '60px' }} />
<WithValidImage
imageSrc={kind.logo.url}
skeletonWidth="60px"
fallback={
<ImageFallback
imageSrc={kind.logo.url}
extended
message="Cannot load logo image"
/>
}
>
{(validSrc) => (
<img src={validSrc} alt={`${kind.name} logo`} style={{ maxWidth: '60px' }} />
)}
</WithValidImage>
</CardHeader>
<CardTitle>{kind.displayName}</CardTitle>
<CardBody>{kind.description}</CardBody>
Expand Down
38 changes: 38 additions & 0 deletions workspaces/frontend/src/shared/components/ImageFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
import { Content, ContentVariants, Flex, FlexItem, Tooltip } from '@patternfly/react-core';

type ImageFallbackProps = {
extended?: boolean;
imageSrc: string | undefined | null;
message?: string;
};

const ImageFallback: React.FC<ImageFallbackProps> = ({
extended = false,
imageSrc,
message = `Cannot load image: ${imageSrc || 'no image source provided'}`,
}) => {
if (extended) {
return (
<Flex alignItems={{ default: 'alignItemsCenter' }} spaceItems={{ default: 'spaceItemsSm' }}>
<FlexItem>
<ExclamationCircleIcon />
</FlexItem>
<FlexItem>
<Content component={ContentVariants.small}>
<i>{message}</i>
</Content>
</FlexItem>
</Flex>
);
}

return (
<Tooltip content={message} position="top">
<ExclamationCircleIcon />
</Tooltip>
);
};

export default ImageFallback;
58 changes: 58 additions & 0 deletions workspaces/frontend/src/shared/components/WithValidImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useEffect, useState } from 'react';
import { Skeleton, SkeletonProps } from '@patternfly/react-core';

type WithValidImageProps = {
imageSrc: string | undefined | null;
fallback: React.ReactNode;
children: (validImageSrc: string) => React.ReactNode;
skeletonWidth?: SkeletonProps['width'];
skeletonShape?: SkeletonProps['shape'];
};

const DEFAULT_SKELETON_WIDTH = '32px';
const DEFAULT_SKELETON_SHAPE: SkeletonProps['shape'] = 'square';

type LoadState = 'loading' | 'valid' | 'invalid';

const WithValidImage: React.FC<WithValidImageProps> = ({
imageSrc,
fallback,
children,
skeletonWidth = DEFAULT_SKELETON_WIDTH,
skeletonShape = DEFAULT_SKELETON_SHAPE,
}) => {
const [status, setStatus] = useState<LoadState>('loading');
const [resolvedSrc, setResolvedSrc] = useState<string>('');

useEffect(() => {
let cancelled = false;

if (!imageSrc) {
setStatus('invalid');
return;
}

const img = new Image();
img.onload = () => !cancelled && (setResolvedSrc(imageSrc), setStatus('valid'));
img.onerror = () => !cancelled && setStatus('invalid');
img.src = imageSrc;

return () => {
cancelled = true;
};
}, [imageSrc]);

if (status === 'loading') {
return (
<Skeleton shape={skeletonShape} width={skeletonWidth} screenreaderText="Loading image" />
);
}

if (status === 'invalid') {
return <>{fallback}</>;
}

return <>{children(resolvedSrc)}</>;
};

export default WithValidImage;