diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 09cf1a0bb9..7f769c2839 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -201,8 +201,6 @@ "Field": "Field", "Value": "Value", "Close": "Close", - "View {{ name }}": "View {{ name }}", - "Edit {{ name }}": "Edit {{ name }}", "Create": "Create", "General info": "General info", "Type and configuration": "Type and configuration", @@ -406,8 +404,6 @@ "Failed to find operating system": "Failed to find operating system", "Failed to find application": "Failed to find application", "Software catalog": "Software catalog", - "Deploy {{ name }}": "Deploy {{ name }}", - "Edit {{name}}": "Edit {{name}}", "Return to device catalog": "Return to device catalog", "Return to fleet catalog": "Return to fleet catalog", "Review update specifications": "Review update specifications", @@ -455,7 +451,6 @@ "Failed to find requested version {{ version }}": "Failed to find requested version {{ version }}", "Invalid channel {{ channel }} for version {{ version }}": "Invalid channel {{ channel }} for version {{ version }}", "Loading catalog item": "Loading catalog item", - "Deploy {{name}}": "Deploy {{name}}", "Application name": "Application name", "Application name must be unique.": "Application name must be unique.", "Configure via:": "Configure via:", @@ -994,7 +989,6 @@ "No assets available in catalog": "No assets available in catalog", "There are no asset catalog items to choose from. Add assets to your catalogs to select them here.": "There are no asset catalog items to choose from. Add assets to your catalogs to select them here.", "Select": "Select", - "Catalog item {{ catalogItemRef }}": "Catalog item {{ catalogItemRef }}", "Enter image reference or choose from catalog": "Enter image reference or choose from catalog", "Choose from catalog": "Choose from catalog", "Approve": "Approve", diff --git a/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/AddCatalogItemWizard.tsx b/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/AddCatalogItemWizard.tsx index e52cc44972..c1e8ad2ca9 100644 --- a/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/AddCatalogItemWizard.tsx +++ b/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/AddCatalogItemWizard.tsx @@ -38,6 +38,8 @@ import { getErrorMessage } from '../../../utils/error'; import { usePermissionsContext } from '../../common/PermissionsContext'; import PageWithPermissions from '../../common/PageWithPermissions'; import { RESOURCE, VERB } from '../../../types/rbac'; +import { CatalogItemLabel } from '../CatalogItemLabels'; +import TruncatedText from '../../common/TruncatedText'; const orderedIds = [generalInfoStepId, typeConfigStepId, versionStepId, reviewStepId]; @@ -109,13 +111,20 @@ const AddCatalogItemWizard = () => { const initialValues = editItem ? getInitialValuesFromItem(editItem) : getInitialValues(); const isReadOnly = !!editItem?.metadata?.owner; - let pageTitle: string; - if (isReadOnly) { - pageTitle = t('View {{ name }}', { name: editItem?.spec.displayName || editItem?.metadata.name }); - } else if (isEdit) { - pageTitle = t('Edit {{ name }}', { name: editItem?.spec.displayName || editItem?.metadata.name }); + let titleEl: React.ReactNode; + if (isReadOnly || isEdit) { + const title = editItem ? ( + + ) : ( + + ); + titleEl = ( + <> + {isReadOnly ? t('View') : t('Edit')} {title} + + ); } else { - pageTitle = t('Create catalog item'); + titleEl = t('Create catalog item'); } let content: React.ReactNode = ( @@ -230,12 +239,12 @@ const AddCatalogItemWizard = () => { {t('Software Catalog')} - {pageTitle} + {titleEl} - {pageTitle} + {titleEl} {content} diff --git a/libs/ui-components/src/components/Catalog/CatalogItemCard.tsx b/libs/ui-components/src/components/Catalog/CatalogItemCard.tsx index 0a7928d926..d7216321ba 100644 --- a/libs/ui-components/src/components/Catalog/CatalogItemCard.tsx +++ b/libs/ui-components/src/components/Catalog/CatalogItemCard.tsx @@ -15,7 +15,8 @@ import * as React from 'react'; import { type CatalogItem, CatalogItemCategory } from '@flightctl/types/alpha'; import { useTranslation } from '../../hooks/useTranslation'; -import { getCatalogItemBadge, getCatalogItemIcon } from '../../utils/catalog'; +import { getCatalogItemBadge, getCatalogItemIcon, getCatalogItemLabel } from '../../utils/catalog'; +import { CatalogItemLabel } from './CatalogItemLabels'; export type CatalogItemCardProps = { catalogItem: CatalogItem; @@ -31,7 +32,7 @@ const CatalogItemCard: React.FC = ({ catalogItem, onSelect onClickAction: onSelect, onChange: onSelect, selectableActionAriaLabel: t('Select {{ name }}', { - name: catalogItem.spec.displayName || catalogItem.metadata.name, + name: getCatalogItemLabel(catalogItem), }), }} > @@ -57,8 +58,10 @@ const CatalogItemCard: React.FC = ({ catalogItem, onSelect - - {catalogItem.spec.displayName || catalogItem.metadata.name} + + + <CatalogItemLabel item={catalogItem} shortened /> + {catalogItem.spec.provider && ( diff --git a/libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx b/libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx index 9096b0a4d0..9dd9899072 100644 --- a/libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx +++ b/libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx @@ -35,9 +35,10 @@ import { ROUTE, useNavigate } from '../../hooks/useNavigate'; import { useItemIsInUse } from './useCatalogItems'; import FlightCtlForm from '../form/FlightCtlForm'; import { DeprecateModal, RestoreModal } from './DeprecateModal'; -import { getCatalogItemIcon, getFullContainerURI } from '../../utils/catalog'; +import { getCatalogItemIcon, getCatalogItemLabel, getFullContainerURI } from '../../utils/catalog'; import DeleteModal from '../modals/DeleteModal/DeleteModal'; import WithTooltip from '../common/WithTooltip'; +import { CatalogItemLabel } from './CatalogItemLabels'; import { buildAllDropdownActions } from '../common/ActionsDropdownList'; import FlightCtlPageDrawer from '../common/FlightCtlPageDrawer'; import { InstallSpec } from './InstallWizard/steps/SpecificationsStep'; @@ -73,7 +74,9 @@ export const CatalogItemDetailsHeader = ({ item }: CatalogItemDetailsHeaderProps {`${item.metadata.name} - {item.spec.displayName || item.metadata.name} + + <CatalogItemLabel item={item} shortened /> + {item.spec.provider && ( {t('Provided by {{provider}}', { provider: item.spec.provider })} @@ -102,7 +105,7 @@ const CatalogItemDetailsModal = ({ const { t } = useTranslation(); const { patch, remove } = useFetch(); - const displayName = item.spec.displayName || (item.metadata.name as string); + const displayName = getCatalogItemLabel(item); const itemEndpoint = `catalogs/${item.metadata.catalog}/items/${item.metadata.name}`; switch (itemModalOpen) { diff --git a/libs/ui-components/src/components/Catalog/CatalogItemLabels.tsx b/libs/ui-components/src/components/Catalog/CatalogItemLabels.tsx new file mode 100644 index 0000000000..6ea7b7fa19 --- /dev/null +++ b/libs/ui-components/src/components/Catalog/CatalogItemLabels.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; + +import type { CatalogItem } from '@flightctl/types/alpha'; +import type { CatalogItemRefSpec } from '@flightctl/types'; + +import { formatCatalogItemRef, getCatalogItemLabel } from '../../utils/catalog'; +import TruncatedText from '../common/TruncatedText'; + +export const CatalogItemLabel = ({ item, shortened }: { item: CatalogItem; shortened?: boolean }) => ( + +); + +export const CatalogItemRefLabel = ({ catalogItemRef }: { catalogItemRef: CatalogItemRefSpec }) => { + const { version, catalog, item } = catalogItemRef; + const text = version ? formatCatalogItemRef(catalogItemRef) : `${catalog}/${item}`; + return ; +}; diff --git a/libs/ui-components/src/components/Catalog/CatalogItemTitle.tsx b/libs/ui-components/src/components/Catalog/CatalogItemTitle.tsx index 88e1113d26..5da2ea88b3 100644 --- a/libs/ui-components/src/components/Catalog/CatalogItemTitle.tsx +++ b/libs/ui-components/src/components/Catalog/CatalogItemTitle.tsx @@ -7,6 +7,7 @@ import type { CatalogItemRefSpec } from '@flightctl/types'; import type { CatalogItem } from '@flightctl/types/alpha'; import { useTranslation } from '../../hooks/useTranslation'; import { getCatalogItemIcon } from '../../utils/catalog'; +import { CatalogItemLabel, CatalogItemRefLabel } from './CatalogItemLabels'; const formatVersionLine = (t: TFunction, version?: string, channel?: string) => { if (!version) { @@ -79,7 +80,9 @@ export const BrokenCatalogItemTitle = ({ )} /> - {`${catalogRef.catalog}/${catalogRef.item}`} + + + } description={description} @@ -102,7 +105,7 @@ const CatalogItemTitle = ({ }) => ( } - title={item.spec.displayName || item.metadata.name || ''} + title={} description={description} version={version} channel={channel} diff --git a/libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx b/libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx index 15fb52de90..3d7335bfb0 100644 --- a/libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx +++ b/libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx @@ -34,6 +34,8 @@ import { hasPackageModeCapability } from '../../../utils/capabilities'; import { appendJSONPatch } from '../../../utils/patch'; import EditOsWizard from './EditOsWizard'; import EditAppWizard from './EditAppWizard'; +import { CatalogItemLabel } from '../CatalogItemLabels'; +import TruncatedText from '../../common/TruncatedText'; type EditWizardProps = { specPath: string; @@ -188,7 +190,11 @@ const EditWizard = ({ } } - const catalogDisplayName = catalogItem?.spec.displayName || params.itemId; + const titleEl = catalogItem ? ( + + ) : ( + + ); return ( <> @@ -209,16 +215,17 @@ const EditWizard = ({ {t('Software catalog')} - {`${catalogDisplayName}${appName ? ` (${appName})` : ''}`} + + {titleEl} + {appName ? ` (${appName})` : ''} + - {version - ? t('Deploy {{ name }}', { name: catalogDisplayName }) - : t('Edit {{name}}', { name: catalogDisplayName })} + {version ? t('Deploy') : t('Edit')} {titleEl} diff --git a/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx b/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx index ae66b54c27..d65d32e0ed 100644 --- a/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx +++ b/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx @@ -25,6 +25,8 @@ import { getErrorMessage } from '../../../utils/error'; import { usePermissionsContext } from '../../common/PermissionsContext'; import PageWithPermissions from '../../common/PageWithPermissions'; import { RESOURCE, VERB } from '../../../types/rbac'; +import { CatalogItemLabel } from '../CatalogItemLabels'; +import TruncatedText from '../../common/TruncatedText'; const InstallWizard = () => { const { t } = useTranslation(); @@ -49,6 +51,12 @@ const InstallWizard = () => { content = ; } + const titleEl = catalogItem ? ( + + ) : ( + + ); + return ( <> @@ -56,14 +64,14 @@ const InstallWizard = () => { {t('Software Catalog')} - {catalogItem?.spec.displayName || params.itemId} + {titleEl} - {t('Deploy {{name}}', { name: catalogItem?.spec.displayName || params.itemId })} + {t('Deploy')} {titleEl} diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/SystemImageDescriptionGroup.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/SystemImageDescriptionGroup.tsx index 2b5c485a24..e917d50102 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/SystemImageDescriptionGroup.tsx +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/SystemImageDescriptionGroup.tsx @@ -11,7 +11,7 @@ import { import type { CatalogItemRefSpec, ImageOrCatalogItemRefSpec } from '@flightctl/types'; import { useTranslation } from '../../../hooks/useTranslation'; -import { formatCatalogItemRef } from '../../../utils/catalog'; +import { CatalogItemRefLabel } from '../../Catalog/CatalogItemLabels'; import { useSystemImage } from './useSystemImage'; export const SystemImageCatalogLabel = () => { @@ -23,28 +23,24 @@ export const SystemImageCatalogLabel = () => { ); }; -export const SystemImageDisplay = ({ - catalogItemRef, - imageUri, -}: { - catalogItemRef: CatalogItemRefSpec | undefined; - imageUri?: string; -}) => { - if (catalogItemRef) { - return ( - - - - - {imageUri || formatCatalogItemRef(catalogItemRef)} - - ); +export const SystemImageDisplay = ({ catalogItemRef }: { catalogItemRef: CatalogItemRefSpec | undefined }) => { + if (!catalogItemRef) { + return '-'; } - return imageUri || '-'; + return ( + + + + + + + + + ); }; const SystemImageDescriptionGroup = ({ @@ -62,7 +58,9 @@ const SystemImageDescriptionGroup = ({ if (imageResult.isLoading) { osContent = ; } else if (imageResult.imageUri || isCatalogItemRef) { - osContent = imageResult.imageUri || formatCatalogItemRef(osSpec?.catalogItemRef as CatalogItemRefSpec); + osContent = imageResult.imageUri || ( + + ); } else { osContent = isFleet ? t('The fleet will not manage system image') : t('The device will not manage system image'); } diff --git a/libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx b/libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx index a0dc68c067..a4c5c71fd6 100644 --- a/libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx +++ b/libs/ui-components/src/components/DynamicForm/VolumeImageField.tsx @@ -48,11 +48,12 @@ import { import FlightCtlForm from '../form/FlightCtlForm'; import { type PaginationDetails } from '../../hooks/useTablePagination'; import { getErrorMessage } from '../../utils/error'; -import { buildCatalogItemRef, formatCatalogItemRef } from '../../utils/catalog'; +import { buildCatalogItemRef } from '../../utils/catalog'; import ResourceListEmptyState from '../common/ResourceListEmptyState'; import FlightCtlModal from '../common/FlightCtlModal'; import { InstallSpec } from '../Catalog/InstallWizard/steps/SpecificationsStep'; import CatalogItemTitle from '../Catalog/CatalogItemTitle'; +import { CatalogItemRefLabel } from '../Catalog/CatalogItemLabels'; import { type InstallSpecFormik } from '../Catalog/InstallWizard/types'; import { useCatalogItems } from '../Catalog/useCatalogItems'; import { useResolvedCatalogRef } from '../Catalog/useResolvedCatalogRef'; @@ -330,9 +331,7 @@ const VolumeImageField = ({ idSchema, formData, onChange, rawErrors, formContext {catalogItem ? ( ) : ( - t('Catalog item {{ catalogItemRef }}', { - catalogItemRef: formatCatalogItemRef(catalogRef), - }) + )} diff --git a/libs/ui-components/src/components/Fleet/FleetRow.tsx b/libs/ui-components/src/components/Fleet/FleetRow.tsx index 1f9bd5edd1..dfaff3512a 100644 --- a/libs/ui-components/src/components/Fleet/FleetRow.tsx +++ b/libs/ui-components/src/components/Fleet/FleetRow.tsx @@ -103,7 +103,7 @@ const FleetRow: React.FC = ({ - + {fleetOsSpec?.image || } {imagePromotion.metadata.name || ''} {imagePromotion.spec.target.catalogName} - {imagePromotion.spec.target.catalogItemName} + + + diff --git a/libs/ui-components/src/components/common/ResourceLink.css b/libs/ui-components/src/components/common/ResourceLink.css deleted file mode 100644 index e0d81363d9..0000000000 --- a/libs/ui-components/src/components/common/ResourceLink.css +++ /dev/null @@ -1,29 +0,0 @@ -.fctl-resource-link { - display: inline-flex; - align-items: center; -} - -.fctl-resource-link.fctl-resource-link__full { - display: flex; -} - -.fctl-resource-link .fctl-resource-link__text { - text-wrap: nowrap; - overflow-x: hidden; -} - -.fctl-resource-link .pf-v6-c-button { - /* The copy name button is hidden by default */ - clip: rect(0 0 0 0); - clip-path: inset(50%); - overflow: hidden; - white-space: nowrap; -} - -.fctl-resource-link:hover .pf-v6-c-button { - /* Makes the copy name button visible when hovering over the name */ - clip: initial; - clip-path: initial; - overflow: visible; - white-space: initial; -} diff --git a/libs/ui-components/src/components/common/ResourceLink.tsx b/libs/ui-components/src/components/common/ResourceLink.tsx index f0075f0355..c6b08013de 100644 --- a/libs/ui-components/src/components/common/ResourceLink.tsx +++ b/libs/ui-components/src/components/common/ResourceLink.tsx @@ -1,54 +1,32 @@ import * as React from 'react'; import { Link, type RouteWithPostfix } from '../../hooks/useNavigate'; -import CopyButton from './CopyButton'; +import TruncatedText from './TruncatedText'; -import './ResourceLink.css'; - -const maxDisplayLength = 50; +export { getDisplayText } from '../../utils/displayText'; type ResourceDisplayLinkProps = { id: string; name?: string; - variant?: 'shortened' | 'full'; routeLink?: RouteWithPostfix; 'data-testid'?: string; }; -export const getDisplayText = (name: string | undefined) => { - if (!name) { - return '-'; - } - if (name.length <= maxDisplayLength) { - return name; - } - return `${name.substring(0, 6)}...${name.substring(name.length - 7)}`; -}; - -const ResourceLink = ({ - id, - name, - variant = 'shortened', - routeLink, - 'data-testid': dataTestId, -}: ResourceDisplayLinkProps) => { +const ResourceLink = ({ id, name, routeLink, 'data-testid': dataTestId }: ResourceDisplayLinkProps) => { const nameOrId = name || id; - const displayText = getDisplayText(nameOrId); - const showCopy = nameOrId !== displayText; - - const textEl = {variant === 'full' ? nameOrId : displayText}; return ( - - {routeLink ? ( - - {textEl} - - ) : ( - {textEl} - )} - {showCopy && nameOrId && } - + + {(textContent) => + routeLink ? ( + + {textContent} + + ) : ( + {textContent} + ) + } + ); }; diff --git a/libs/ui-components/src/components/common/TruncatedText.css b/libs/ui-components/src/components/common/TruncatedText.css new file mode 100644 index 0000000000..ff8b0e90b9 --- /dev/null +++ b/libs/ui-components/src/components/common/TruncatedText.css @@ -0,0 +1,35 @@ +.fctl-truncated-text { + display: inline-flex; + align-items: center; + max-width: 100%; + min-width: 0; +} + +.fctl-truncated-text__copy { + text-wrap: nowrap; + overflow-x: hidden; +} + +.fctl-truncated-text .pf-v6-c-button { + /* The copy button is hidden by default */ + clip-path: inset(50%); + overflow: hidden; + white-space: nowrap; +} + +.fctl-truncated-text:hover .pf-v6-c-button, +.fctl-truncated-text:focus-within .pf-v6-c-button { + /* Reveal the copy button on hover or when a descendant has keyboard focus */ + clip-path: none; + overflow: visible; + white-space: initial; +} + +@media (hover: none) { + .fctl-truncated-text .pf-v6-c-button { + /* Touch devices have no reliable hover; keep the copy button visible */ + clip-path: none; + overflow: visible; + white-space: initial; + } +} diff --git a/libs/ui-components/src/components/common/TruncatedText.tsx b/libs/ui-components/src/components/common/TruncatedText.tsx new file mode 100644 index 0000000000..46abc4f8a6 --- /dev/null +++ b/libs/ui-components/src/components/common/TruncatedText.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; + +import { defaultMaxDisplayLength, getDisplayText } from '../../utils/displayText'; +import CopyButton from './CopyButton'; + +import './TruncatedText.css'; + +type TruncatedTextProps = { + text: string; + showCopy?: boolean; + maxChars?: number; + leadingChars?: number; + children?: (textContent: string) => React.ReactNode; +}; + +const TruncatedText = ({ + text, + showCopy, + leadingChars, + maxChars = defaultMaxDisplayLength, + children, +}: TruncatedTextProps) => { + const displayText = getDisplayText(text, maxChars, leadingChars); + const shouldShowCopy = showCopy ?? text !== displayText; + + return ( + + {children ? children(displayText) : displayText} + {shouldShowCopy && } + + ); +}; + +export default TruncatedText; diff --git a/libs/ui-components/src/utils/catalog.ts b/libs/ui-components/src/utils/catalog.ts index 1567fd38f4..779b8a50a5 100644 --- a/libs/ui-components/src/utils/catalog.ts +++ b/libs/ui-components/src/utils/catalog.ts @@ -59,6 +59,9 @@ export const catalogItemCacheKey = (id: CatalogItemId): string => `${id.catalog} export const formatCatalogItemRef = (ref: CatalogItemRefSpec): string => `${ref.catalog}/${ref.item}:${ref.version}`; +/** User-facing catalog item label: display name when set, otherwise system name. */ +export const getCatalogItemLabel = (item: CatalogItem): string => item.spec.displayName || item.metadata.name || ''; + export const toCatalogItemId = (ref: Pick): CatalogItemId => ({ catalog: ref.catalog, item: ref.item, @@ -172,7 +175,7 @@ export const getFullContainerURI = (artifacts: CatalogItemArtifact[], version: C export const resolveCatalogRef = (item: CatalogItem, ref: CatalogItemRefSpec): ResolvedCatalogRef => { const version = getCurrentVersion(item, ref.version, ref); - const displayName = item.spec.displayName || item.metadata.name || ref.item; + const displayName = getCatalogItemLabel(item) || ref.item; const imageUri = version ? getFullContainerURI(item.spec.artifacts, version) : undefined; return { item, diff --git a/libs/ui-components/src/utils/displayText.ts b/libs/ui-components/src/utils/displayText.ts new file mode 100644 index 0000000000..62a6df8dac --- /dev/null +++ b/libs/ui-components/src/utils/displayText.ts @@ -0,0 +1,12 @@ +/** EDM-4074: default max length before middle-ellipsis shortening applies. */ +export const defaultMaxDisplayLength = 50; + +export const getDisplayText = (text: string | undefined, maxLength = defaultMaxDisplayLength, leadingChars = 6) => { + if (!text) { + return '-'; + } + if (text.length <= maxLength) { + return text; + } + return `${text.substring(0, leadingChars)}...${text.substring(text.length - 7)}`; +};