diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json
index 9cfc8004b..b0b22de34 100644
--- a/libs/i18n/locales/en/translation.json
+++ b/libs/i18n/locales/en/translation.json
@@ -205,8 +205,8 @@
"Field": "Field",
"Value": "Value",
"Close": "Close",
- "View {{ name }}": "View {{ name }}",
- "Edit {{ name }}": "Edit {{ name }}",
+ "View {catalogItemEl}": "View {catalogItemEl}",
+ "Edit {catalogItemEl}": "Edit {catalogItemEl}",
"Create": "Create",
"General info": "General info",
"Type and configuration": "Type and configuration",
@@ -414,9 +414,8 @@
"Device is in package mode. The catalog item cannot be deployed to this device.": "Device is in package mode. The catalog item cannot be deployed to this device.",
"Failed to find operating system": "Failed to find operating system",
"Failed to find application": "Failed to find application",
+ "Deploy {catalogItemEl}": "Deploy {catalogItemEl}",
"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",
@@ -462,7 +461,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:",
diff --git a/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/AddCatalogItemWizard.tsx b/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/AddCatalogItemWizard.tsx
index e52cc4497..77213b3f3 100644
--- a/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/AddCatalogItemWizard.tsx
+++ b/libs/ui-components/src/components/Catalog/AddCatalogItemWizard/AddCatalogItemWizard.tsx
@@ -11,6 +11,7 @@ import {
WizardStep,
type WizardStepType,
} from '@patternfly/react-core';
+import { Trans } from 'react-i18next';
import { type CatalogItem } from '@flightctl/types/alpha';
import { Formik, type FormikErrors } from 'formik';
@@ -38,6 +39,7 @@ import { getErrorMessage } from '../../../utils/error';
import { usePermissionsContext } from '../../common/PermissionsContext';
import PageWithPermissions from '../../common/PageWithPermissions';
import { RESOURCE, VERB } from '../../../types/rbac';
+import { CatalogItemLabel } from '../CatalogItemDetails';
const orderedIds = [generalInfoStepId, typeConfigStepId, versionStepId, reviewStepId];
@@ -109,13 +111,22 @@ const AddCatalogItemWizard = () => {
const initialValues = editItem ? getInitialValuesFromItem(editItem) : getInitialValues();
const isReadOnly = !!editItem?.metadata?.owner;
- let pageTitle: string;
+ const catalogItemEl = ;
+ let titleEl: React.ReactNode;
if (isReadOnly) {
- pageTitle = t('View {{ name }}', { name: editItem?.spec.displayName || editItem?.metadata.name });
+ titleEl = (
+
+ View {catalogItemEl}
+
+ );
} else if (isEdit) {
- pageTitle = t('Edit {{ name }}', { name: editItem?.spec.displayName || editItem?.metadata.name });
+ titleEl = (
+
+ Edit {catalogItemEl}
+
+ );
} else {
- pageTitle = t('Create catalog item');
+ titleEl = t('Create catalog item');
}
let content: React.ReactNode = (
@@ -230,12 +241,12 @@ const AddCatalogItemWizard = () => {
{t('Software Catalog')}
- {pageTitle}
+ {titleEl}
- {pageTitle}
+ {titleEl}
{content}
diff --git a/libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx b/libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx
index 809179fbb..f36a5ee64 100644
--- a/libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx
+++ b/libs/ui-components/src/components/Catalog/CatalogItemDetails.tsx
@@ -41,6 +41,7 @@ import DeleteModal from '../modals/DeleteModal/DeleteModal';
import WithTooltip from '../common/WithTooltip';
import { buildAllDropdownActions } from '../common/ActionsDropdownList';
import FlightCtlPageDrawer from '../common/FlightCtlPageDrawer';
+import TruncatedText from '../common/TruncatedText';
import CatalogItemIcon from './CatalogItemIcon';
import { InstallSpec } from './InstallWizard/steps/SpecificationsStep';
import { type InstallSpecFormik } from './InstallWizard/types';
@@ -67,6 +68,21 @@ type CatalogItemDetailsHeaderProps = {
item: CatalogItem;
};
+export const CatalogItemLabel = ({
+ item,
+ fallbackId,
+ shortened,
+}: {
+ item?: CatalogItem;
+ fallbackId?: string;
+ shortened?: boolean;
+}) => (
+
+);
+
export const CatalogItemDetailsHeader = ({ item }: CatalogItemDetailsHeaderProps) => {
const { t } = useTranslation();
return (
@@ -75,7 +91,9 @@ export const CatalogItemDetailsHeader = ({ item }: CatalogItemDetailsHeaderProps
- {item.spec.displayName || item.metadata.name}
+
+
+
{item.spec.provider && (
{t('Provided by {{provider}}', { provider: item.spec.provider })}
diff --git a/libs/ui-components/src/components/Catalog/CatalogItemTitle.tsx b/libs/ui-components/src/components/Catalog/CatalogItemTitle.tsx
index da91e6036..945bf973f 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 CatalogItemIcon from './CatalogItemIcon';
+import { CatalogItemLabel } from './CatalogItemDetails';
const formatVersionLine = (t: TFunction, version?: string, channel?: string) => {
if (!version) {
@@ -90,25 +91,26 @@ export const BrokenCatalogItemTitle = ({
};
const CatalogItemTitle = ({
- headerTitle,
item,
+ title,
version,
channel,
- description,
}: {
item: CatalogItem;
- headerTitle: string;
+ title?: string;
version?: string;
channel?: string;
- description?: string;
-}) => (
- }
- title={headerTitle}
- description={description}
- version={version}
- channel={channel}
- />
-);
+}) => {
+ const nameEl = ;
+ return (
+ }
+ title={title || nameEl}
+ description={title ? nameEl : undefined}
+ version={version}
+ channel={channel}
+ />
+ );
+};
export default CatalogItemTitle;
diff --git a/libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx b/libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx
index 5c51eeef4..5b540bd21 100644
--- a/libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx
+++ b/libs/ui-components/src/components/Catalog/EditWizard/EditWizard.tsx
@@ -7,6 +7,8 @@ import {
Content,
ContentVariants,
EmptyState,
+ Flex,
+ FlexItem,
PageSection,
Spinner,
Stack,
@@ -15,6 +17,7 @@ import {
} from '@patternfly/react-core';
import { CatalogItemCategory } from '@flightctl/types/alpha';
import { load } from 'js-yaml';
+import { Trans } from 'react-i18next';
import type { ApplicationProviderSpec, Device, Fleet, ImageOrCatalogItemRefSpec, PatchRequest } from '@flightctl/types';
import ErrorBoundary from '../../common/ErrorBoundary';
@@ -35,6 +38,7 @@ import { appendJSONPatch } from '../../../utils/patches/patch';
import { type CatalogEditWizardMode, getEditWizardMode } from '../../../utils/catalog';
import EditOsWizard from './EditOsWizard';
import EditAppWizard from './EditAppWizard';
+import { CatalogItemLabel } from '../CatalogItemDetails';
type EditWizardProps = {
mode: CatalogEditWizardMode;
@@ -192,7 +196,21 @@ const EditWizard = ({
}
}
- const catalogDisplayName = catalogItem?.spec.displayName || params.itemId;
+ const catalogItemEl = ;
+ let titleEl: React.ReactNode;
+ if (version) {
+ titleEl = (
+
+ Deploy {catalogItemEl}
+
+ );
+ } else {
+ titleEl = (
+
+ Edit {catalogItemEl}
+
+ );
+ }
return (
<>
@@ -213,16 +231,19 @@ const EditWizard = ({
{t('Software catalog')}
- {`${catalogDisplayName}${appName ? ` (${appName})` : ''}`}
+
+
+ {appName ? `(${appName}) ` : ''}
+ {titleEl}
+
+
- {version
- ? t('Deploy {{ name }}', { name: catalogDisplayName })
- : t('Edit {{name}}', { name: catalogDisplayName })}
+ {titleEl}
diff --git a/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx b/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx
index ae66b54c2..49b938c0c 100644
--- a/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx
+++ b/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx
@@ -25,6 +25,7 @@ import { getErrorMessage } from '../../../utils/error';
import { usePermissionsContext } from '../../common/PermissionsContext';
import PageWithPermissions from '../../common/PageWithPermissions';
import { RESOURCE, VERB } from '../../../types/rbac';
+import { CatalogItemLabel } from '../CatalogItemDetails';
const InstallWizard = () => {
const { t } = useTranslation();
@@ -49,6 +50,8 @@ const InstallWizard = () => {
content = ;
}
+ const titleEl = ;
+
return (
<>
@@ -56,14 +59,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/Catalog/InstallWizard/steps/SpecificationsStep.tsx b/libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx
index 78728cb9c..8863f117f 100644
--- a/libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx
+++ b/libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx
@@ -25,7 +25,6 @@ import {
import FlightCtlModal from '@flightctl/ui-components/src/components/common/FlightCtlModal';
import * as React from 'react';
import { type FormikErrors, useFormikContext } from 'formik';
-import * as semver from 'semver';
import ReactMarkdown from 'react-markdown';
import { type TFunction } from 'react-i18next';
@@ -42,6 +41,7 @@ import { applyInitialConfig, getInitialAppConfig } from '../utils';
import { type InstallAppFormik, type InstallSpecFormik, type TargetPickerFormik } from '../types';
import WithTooltip from '../../../common/WithTooltip';
import { getFullContainerURI } from '../../../../utils/catalog';
+import { getSortedChannelVersions } from '../../../CatalogComposition/catalogCompositionUtils';
type VersionDropdownProps = {
catalogItem: CatalogItem;
@@ -106,14 +106,11 @@ export const InstallSpec = ({
return acc;
}, {});
- const versions = catalogItem.spec.versions.sort((v1, v2) => semver.compare(v2.version, v1.version));
-
- const channelVersions = versions.filter(
- (v) =>
- v.channels.includes(values.channel) && (targetSet ? !!getFullContainerURI(catalogItem.spec.artifacts, v) : true),
+ const channelVersions = getSortedChannelVersions(catalogItem, values.channel).filter((v) =>
+ targetSet ? !!getFullContainerURI(catalogItem.spec.artifacts, v) : true,
);
- const currentVersion = versions.find((v) => v.version === values.version);
+ const currentVersion = catalogItem.spec.versions.find((v) => v.version === values.version);
return (
<>
@@ -131,7 +128,7 @@ export const InstallSpec = ({
name="channel"
items={channels}
onChange={(val) => {
- const newChannelVersions = versions.filter((v) => v.channels.includes(val));
+ const newChannelVersions = getSortedChannelVersions(catalogItem, val);
if (!newChannelVersions.some((v) => v.version === values.version)) {
const newVersion = newChannelVersions.length ? newChannelVersions[0].version : undefined;
setFieldValue('version', newVersion, true);
diff --git a/libs/ui-components/src/components/Catalog/InstalledSoftwareItem.tsx b/libs/ui-components/src/components/Catalog/InstalledSoftwareItem.tsx
index 3b1f50065..8b7523462 100644
--- a/libs/ui-components/src/components/Catalog/InstalledSoftwareItem.tsx
+++ b/libs/ui-components/src/components/Catalog/InstalledSoftwareItem.tsx
@@ -23,16 +23,7 @@ const SoftwareItemTitle = ({
return ;
}
- const itemName = (data.item.spec.displayName || data.item.metadata.name) as string;
- return (
-
- );
+ return ;
};
const SoftwareItemUpdateBadge = ({
diff --git a/libs/ui-components/src/components/CatalogComposition/catalogCompositionUtils.ts b/libs/ui-components/src/components/CatalogComposition/catalogCompositionUtils.ts
index 6f016f767..b531946e7 100644
--- a/libs/ui-components/src/components/CatalogComposition/catalogCompositionUtils.ts
+++ b/libs/ui-components/src/components/CatalogComposition/catalogCompositionUtils.ts
@@ -157,7 +157,7 @@ export const getDefaultChannelAndVersion = (item: CatalogItem) => {
};
}
- const versions = item.spec.versions.sort((v1, v2) => semver.rcompare(v1.version, v2.version));
+ const versions = [...item.spec.versions].sort((v1, v2) => semver.rcompare(v1.version, v2.version));
// release then prerelease
const latestVersion = versions.find((v) => !semver.prerelease(v.version)) || versions[0];
diff --git a/libs/ui-components/src/components/QuickStart/quickStartDefinitions.ts b/libs/ui-components/src/components/QuickStart/quickStartDefinitions.ts
index a84335874..415ffb518 100644
--- a/libs/ui-components/src/components/QuickStart/quickStartDefinitions.ts
+++ b/libs/ui-components/src/components/QuickStart/quickStartDefinitions.ts
@@ -1,4 +1,4 @@
-import type { TFunction } from 'react-i18next';
+import type { TFunction } from 'i18next';
import { RESOURCE, VERB } from '../../types/rbac';
import type { PermissionCheck } from '../common/PermissionsContext';
import type { QuickStartPhaseDefinition } from './types';
diff --git a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryGeneralDetailsCard.tsx b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryGeneralDetailsCard.tsx
index 28b5035d6..c4b090549 100644
--- a/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryGeneralDetailsCard.tsx
+++ b/libs/ui-components/src/components/Repository/RepositoryDetails/RepositoryGeneralDetailsCard.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import type { TFunction } from 'react-i18next';
+import type { TFunction } from 'i18next';
import {
Card,
CardBody,
diff --git a/libs/ui-components/src/components/Terminal/AppTerminal.tsx b/libs/ui-components/src/components/Terminal/AppTerminal.tsx
index 7f234807d..41e185470 100644
--- a/libs/ui-components/src/components/Terminal/AppTerminal.tsx
+++ b/libs/ui-components/src/components/Terminal/AppTerminal.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import type { TFunction } from 'react-i18next';
+import type { TFunction } from 'i18next';
import {
Alert,
AlertActionLink,
diff --git a/libs/ui-components/src/components/Terminal/TerminalConnectError.tsx b/libs/ui-components/src/components/Terminal/TerminalConnectError.tsx
index 65b24752f..f8859a041 100644
--- a/libs/ui-components/src/components/Terminal/TerminalConnectError.tsx
+++ b/libs/ui-components/src/components/Terminal/TerminalConnectError.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Alert, AlertActionLink } from '@patternfly/react-core';
-import type { TFunction } from 'react-i18next';
+import type { TFunction } from 'i18next';
import type { AppConsoleConnectError } from '../../hooks/useAppConsoleWebSocket';
import { useTranslation } from '../../hooks/useTranslation';
diff --git a/libs/ui-components/src/components/common/ResourceLink.tsx b/libs/ui-components/src/components/common/ResourceLink.tsx
index f0075f035..469c91cda 100644
--- a/libs/ui-components/src/components/common/ResourceLink.tsx
+++ b/libs/ui-components/src/components/common/ResourceLink.tsx
@@ -1,12 +1,11 @@
import * as React from 'react';
import { Link, type RouteWithPostfix } from '../../hooks/useNavigate';
+import { getDisplayText } from '../../utils/displayText';
import CopyButton from './CopyButton';
import './ResourceLink.css';
-const maxDisplayLength = 50;
-
type ResourceDisplayLinkProps = {
id: string;
name?: string;
@@ -15,16 +14,6 @@ type ResourceDisplayLinkProps = {
'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,
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 000000000..ff8b0e90b
--- /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 000000000..cd113e86c
--- /dev/null
+++ b/libs/ui-components/src/components/common/TruncatedText.tsx
@@ -0,0 +1,34 @@
+import * as React from 'react';
+
+import CopyButton from './CopyButton';
+import { defaultMaxDisplayLength, getDisplayText } from '../../utils/displayText';
+
+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/components/modals/DeleteModal/DeleteModal.tsx b/libs/ui-components/src/components/modals/DeleteModal/DeleteModal.tsx
index 7fb819b9b..a64f3eafc 100644
--- a/libs/ui-components/src/components/modals/DeleteModal/DeleteModal.tsx
+++ b/libs/ui-components/src/components/modals/DeleteModal/DeleteModal.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import type { TFunction } from 'react-i18next';
+import type { TFunction } from 'i18next';
import { Alert, Button, ModalBody, ModalFooter, ModalHeader, Stack, StackItem } from '@patternfly/react-core';
import { ResourceKind } from '@flightctl/types';
diff --git a/libs/ui-components/src/hooks/useDeviceSpecSystemInfo.tsx b/libs/ui-components/src/hooks/useDeviceSpecSystemInfo.tsx
index 1ab8835d7..a9281356a 100644
--- a/libs/ui-components/src/hooks/useDeviceSpecSystemInfo.tsx
+++ b/libs/ui-components/src/hooks/useDeviceSpecSystemInfo.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import type { TFunction } from 'react-i18next';
+import type { TFunction } from 'i18next';
import type { DeviceSystemInfo } from '@flightctl/types';
diff --git a/libs/ui-components/src/utils/displayText.ts b/libs/ui-components/src/utils/displayText.ts
new file mode 100644
index 000000000..62a6df8da
--- /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)}`;
+};