From bf742960303ddf063e05b3deaa0bfbdf261971af Mon Sep 17 00:00:00 2001 From: rawagner Date: Wed, 4 Mar 2026 09:55:44 +0100 Subject: [PATCH 1/2] EDM-3293: Update chart improvements --- apps/ocp-plugin/package.json | 1 - apps/standalone/package.json | 1 - libs/i18n/locales/en/translation.json | 25 +- libs/ui-components/package.json | 1 - .../Catalog/EditWizard/EditAppWizard.tsx | 23 +- .../Catalog/EditWizard/EditOsWizard.tsx | 2 +- .../Catalog/EditWizard/steps/ReviewStep.tsx | 2 +- .../Catalog/EditWizard/steps/UpdateGraph.css | 142 +++++- .../Catalog/EditWizard/steps/UpdateGraph.tsx | 422 ++++++------------ .../Catalog/EditWizard/steps/UpdateStep.tsx | 218 +++++---- .../InstallWizard/InstallAppWizard.tsx | 2 +- .../Catalog/InstallWizard/InstallOsWizard.tsx | 2 +- .../Catalog/InstallWizard/InstallWizard.tsx | 2 +- .../InstallWizard/steps/ReviewStep.tsx | 2 +- .../InstallWizard/steps/SelectTargetStep.tsx | 2 +- .../steps/SpecificationsStep.tsx | 73 +-- .../components/Catalog/InstalledSoftware.tsx | 211 +++++---- package-lock.json | 3 - 18 files changed, 580 insertions(+), 554 deletions(-) diff --git a/apps/ocp-plugin/package.json b/apps/ocp-plugin/package.json index e67345a5c..1e7b16ac8 100644 --- a/apps/ocp-plugin/package.json +++ b/apps/ocp-plugin/package.json @@ -78,7 +78,6 @@ "@patternfly/react-icons": "^6.4.0", "@patternfly/react-styles": "^6.4.0", "@patternfly/react-table": "^6.4.0", - "@patternfly/react-topology": "^6.4.0", "@types/react-redux": "^7.1.33", "formik": "^2.4.9", "fuzzysearch": "^1.0.3", diff --git a/apps/standalone/package.json b/apps/standalone/package.json index d9171df7a..c0a2a44f0 100644 --- a/apps/standalone/package.json +++ b/apps/standalone/package.json @@ -47,7 +47,6 @@ "@patternfly/react-icons": "^6.4.0", "@patternfly/react-styles": "^6.4.0", "@patternfly/react-table": "^6.4.0", - "@patternfly/react-topology": "^6.4.0", "formik": "^2.4.5", "fuzzysearch": "^1.0.3", "i18next": "^21.8.14", diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index f04be819c..9faf05a2e 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -215,7 +215,7 @@ "Search by name": "Search by name", "Update": "Update", "Version": "Version", - "Review": "Review", + "Review and deploy": "Review and deploy", "Version must be selected": "Version must be selected", "Application name is required": "Application name is required", "Application with the same name already exists.": "Application with the same name already exists.", @@ -234,21 +234,25 @@ "Review update specifications": "Review update specifications", "Review deployment specifications": "Review deployment specifications", "Update specifications": "Update specifications", - "Installation specifications": "Installation specifications", + "Deployment specifications": "Deployment specifications", "Channel": "Channel", "Failed to update": "Failed to update", "Failed to deploy": "Failed to deploy", "Configuration is not valid": "Configuration is not valid", - "The current version is deprecated": "The current version is deprecated", - "The selected version is deprecated": "The selected version is deprecated", + "Other available paths": "Other available paths", + "+ More": "+ More", + "Version {{version}}": "Version {{version}}", + "{{currentChannel}} channel": "{{currentChannel}} channel", "Version update": "Version update", "Current channel": "Current channel", "Target channel": "Target channel", + "The current version is available in the channels listed in the dropdown below": "The current version is available in the channels listed in the dropdown below", "Current version": "Current version", "Target version": "Target version", "Up to date": "Up to date", - "{{ channel }} channel": "{{ channel }} channel", - "Deployment specifications": "Deployment specifications", + "Show readme": "Show readme", + "No version update will be performed": "No version update will be performed", + "This version is deprecated": "This version is deprecated", "Update available": "Update available", "Version: {{version}}, Channel: {{channel}}": "Version: {{version}}, Channel: {{channel}}", "Loading installed software": "Loading installed software", @@ -266,7 +270,7 @@ "Fleet must be selected": "Fleet must be selected", "Failed to find requested version {{version}}": "Failed to find requested version {{version}}", "Loading catalog item": "Loading catalog item", - "Install {{name}}": "Install {{name}}", + "Deploy {{name}}": "Deploy {{name}}", "Application name": "Application name", "Application name must be unique.": "Application name must be unique.", "Configure via:": "Configure via:", @@ -303,8 +307,6 @@ "Cloud native": "Cloud native", "Deployment target": "Deployment target", "No items": "No items", - "Show readme": "Show readme", - "This version is deprecated": "This version is deprecated", "You do not have permissions to list fleets": "You do not have permissions to list fleets", "You do not have permissions to edit fleets": "You do not have permissions to edit fleets", "No fleet is available": "No fleet is available", @@ -313,9 +315,9 @@ "No device is available": "No device is available", "Loading targets": "Loading targets", "Existing Fleet": "Existing Fleet", - "Install to all devices in a fleet": "Install to all devices in a fleet", + "Deploy to all devices in a fleet": "Deploy to all devices in a fleet", "Existing Device": "Existing Device", - "Install to a single fleetless device": "Install to a single fleetless device", + "Deploy to a single fleetless device": "Deploy to a single fleetless device", "New Device": "New Device", "Provision a brand new, unenrolled device": "Provision a brand new, unenrolled device", "Update configuration successful": "Update configuration successful", @@ -750,6 +752,7 @@ "Resourcesync is not accessible": "Resourcesync is not accessible", "Resourcesync new commit detected": "Resourcesync new commit detected", "Resource": "Resource", + "Review": "Review", "Review and create": "Review and create", "Edit fleet": "Edit fleet", "Create fleet": "Create fleet", diff --git a/libs/ui-components/package.json b/libs/ui-components/package.json index 2423de9bb..5ec6b3859 100644 --- a/libs/ui-components/package.json +++ b/libs/ui-components/package.json @@ -56,7 +56,6 @@ "@patternfly/react-icons": "^6.4.0", "@patternfly/react-styles": "^6.4.0", "@patternfly/react-table": "^6.4.0", - "@patternfly/react-topology": "^6.4.0", "i18next": "21.8.14 - 23.x", "monaco-editor": "^0.51.0", "react": "17.0.1 - 18.x", diff --git a/libs/ui-components/src/components/Catalog/EditWizard/EditAppWizard.tsx b/libs/ui-components/src/components/Catalog/EditWizard/EditAppWizard.tsx index 285c371ae..b1160a62b 100644 --- a/libs/ui-components/src/components/Catalog/EditWizard/EditAppWizard.tsx +++ b/libs/ui-components/src/components/Catalog/EditWizard/EditAppWizard.tsx @@ -8,7 +8,7 @@ import { ApplicationProviderSpec } from '@flightctl/types'; import semver from 'semver'; import { useTranslation } from '../../../hooks/useTranslation'; -import { applyInitialConfig, getInitialAppConfig } from '../InstallWizard/utils'; +import { getInitialAppConfig } from '../InstallWizard/utils'; import AppConfigStep, { isAppConfigStepValid } from '../InstallWizard/steps/AppConfigStep'; import FlightCtlWizardFooter from '../../common/FlightCtlWizardFooter'; import { useSubmitCatalogForm } from '../useSubmitCatalogForm'; @@ -39,7 +39,6 @@ type WizardContentProps = { catalogItem: CatalogItem; error: string | undefined; schemaErrors: RJSFValidationError[] | undefined; - currentLabels: Record | undefined; setError: (err: string | undefined) => void; }; @@ -49,13 +48,12 @@ const WizardContent: React.FC = ({ catalogItem, error, schemaErrors, - currentLabels, setError, }) => { const { t } = useTranslation(); const [currentStep, setCurrentStep] = React.useState(); - const { values, errors, setFieldValue } = useFormikContext(); + const { values, errors } = useFormikContext(); const isVersionStepValid = !!values.version; const isConfigStepValid = isAppConfigStepValid(values, errors); @@ -81,21 +79,17 @@ const WizardContent: React.FC = ({ > {(!currentStep || currentStep?.id === versionStepId) && ( - { - const appConfig = getInitialAppConfig(catalogItem, version, appSpec, currentLabels); - applyInitialConfig(setFieldValue, appConfig); - }} - /> + )} {currentStep?.id === configStepId && } - + {currentStep?.id === reviewStepId && ( )} @@ -178,7 +172,6 @@ const EditAppWizard: React.FC = ({ catalogItem={catalogItem} error={error} schemaErrors={schemaErrors} - currentLabels={currentLabels} setError={setError} /> diff --git a/libs/ui-components/src/components/Catalog/EditWizard/EditOsWizard.tsx b/libs/ui-components/src/components/Catalog/EditWizard/EditOsWizard.tsx index bd2e0393f..4c7e48180 100644 --- a/libs/ui-components/src/components/Catalog/EditWizard/EditOsWizard.tsx +++ b/libs/ui-components/src/components/Catalog/EditWizard/EditOsWizard.tsx @@ -62,7 +62,7 @@ const WizardContent: React.FC = ({ currentVersion, catalogIt )} - + {currentStep?.id === reviewStepId && } diff --git a/libs/ui-components/src/components/Catalog/EditWizard/steps/ReviewStep.tsx b/libs/ui-components/src/components/Catalog/EditWizard/steps/ReviewStep.tsx index 9447b5374..f3b234cf5 100644 --- a/libs/ui-components/src/components/Catalog/EditWizard/steps/ReviewStep.tsx +++ b/libs/ui-components/src/components/Catalog/EditWizard/steps/ReviewStep.tsx @@ -40,7 +40,7 @@ const ReviewStep = ({ error, schemaErrors, isEdit }: ReviewStepProps) => { - {isEdit ? t('Update specifications') : t('Installation specifications')} + {isEdit ? t('Update specifications') : t('Deployment specifications')} diff --git a/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.css b/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.css index 998502e98..fcc248735 100644 --- a/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.css +++ b/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.css @@ -1,4 +1,138 @@ -.fctl-update-graph .pf-topology-content { - background-color: unset; - height: 400px; -} \ No newline at end of file +.fctl-channel { + align-items: center; + display: flex; +} + +.fctl-channel-line { + align-items: center; + display: flex; + height: 30px; + justify-content: center; + position: relative; + min-width: 50px; + width: 100%; +} + +.fctl-channel-line::before { + background-color: var(--pf-t--global--border--color--default); + content: ''; + height: 4px; + position: absolute; + width: 100%; +} + +.fctl-channel-line:last-child::after { + background: transparent; + border: 8px solid transparent; + border-left-color: var(--pf-t--global--border--color--default); + border-width: 8px 12px; + content: ''; + position: absolute; + right: -15px; +} + +.fctl-channel-more-versions { + background: var(--pf-t--global--background--color--primary--default) !important; + border: 1px solid !important; + border-radius: 15px !important; + display: inline-block !important; + font-size: 11px !important; + font-weight: bold !important; + justify-content: center; + padding: var(--pf-t--global--spacer--xs) var(--pf-t--global--spacer--sm) !important; + position: absolute !important; +} + +.fctl-channel-more-versions::after { + display: none; +} + +.fctl-channel-more-versions:hover, +.fctl-channel-more-versions:focus { + background: var(--pf-t--global--color--brand--default) !important; + border-color: var(--pf-t--global--color--brand--default) !important; + color: var(--pf-t--global--background--color--primary--default) !important; +} + +.fctl-channel-more-versions:focus { + outline: 0 !important; +} + +.fctl-channel-name { + padding: 0 10px 0 20px; + min-width: 150px; + white-space: nowrap; + color: inherit; + padding-top: 20px; +} + +.fctl-channel-path { + display: flex; + list-style: none; + margin: 0; + padding: 0; + position: relative; + width: 100%; + margin-top: 20px; +} + +.fctl-channel-path .fctl-channel-line::before { + background-color: var(--pf-t--global--color--brand--default); +} + +.fctl-channel-path .fctl-channel-line:last-child::after { + border-left-color: var(--pf-t--global--color--brand--default); +} + +.fctl-channel-version { + display: flex; + flex-direction: column; + height: 35px; + justify-content: flex-end; + line-height: 1; + padding: 0 5px; + position: absolute; + text-align: center; + top: -35px; + overflow-wrap: anywhere; + word-break: normal; +} + +.fctl-channel-version-dot { + background: var(--pf-t--global--color--brand--default) !important; + border-radius: 16px !important; + height: 16px !important; + padding: 0 !important; + position: absolute; + min-width: auto; + width: 16px !important; + z-index: 2; +} + +.fctl-channel-version-dot::after { + background: var(--pf-t--global--background--color--primary--default) !important; + border: 2px solid var(--pf-t--global--background--color--primary--default) !important; + border-radius: 12px !important; + bottom: 2px !important; + content: ''; + left: 2px !important; + position: absolute; + right: 2px !important; + top: 2px !important; +} + +.fctl-channel-version-dot--current::after { + background: transparent !important; +} + +.fctl-channel-version-dot--selected::after { + display: none; +} + +.fctl-channel-version-dot--selected svg { + fill: var(--pf-t--global--background--color--primary--default); + position: relative; + z-index: 1; + width: 100%; + height: 100%; +} diff --git a/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx b/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx index 2b78a2ccf..e36f81129 100644 --- a/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx +++ b/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx @@ -1,312 +1,168 @@ -import { Alert, Card, CardBody, Stack, StackItem } from '@patternfly/react-core'; -import semver from 'semver'; import * as React from 'react'; -import { - ComponentFactory, - DagreLayout, - DefaultEdge, - DefaultNode, - EdgeModel, - EdgeStyle, - GRAPH_LAYOUT_END_EVENT, - Graph, - GraphComponent, - LEFT_TO_RIGHT, - Layout, - LayoutFactory, - Model, - ModelKind, - Node, - NodeModel, - NodeShape, - SELECTION_EVENT, - TopologyControlBar, - TopologyView, - Visualization, - VisualizationProvider, - VisualizationSurface, - WithSelectionProps, - action, - createTopologyControlButtons, - defaultControlButtonsOptions, - observer, - withPanZoom, - withSelection, -} from '@patternfly/react-topology'; +import { useField, useFormikContext } from 'formik'; import { CatalogItemVersion } from '@flightctl/types/alpha'; -import warningColor from '@patternfly/react-tokens/dist/js/t_global_icon_color_status_warning_default'; -import successColor from '@patternfly/react-tokens/dist/js/t_global_icon_color_status_success_default'; -import blueColor from '@patternfly/react-tokens/dist/js/chart_color_blue_400'; -import { ArrowCircleUpIcon, CheckCircleIcon, WarningTriangleIcon } from '@patternfly/react-icons/dist/js/icons'; +import semver from 'semver'; +import { Button, Popover, Stack } from '@patternfly/react-core'; +import { ArrowCircleUpIcon } from '@patternfly/react-icons/dist/js/icons'; import { useTranslation } from '../../../../hooks/useTranslation'; - -import '@patternfly/react-topology/dist/esm/css/topology-components.css'; -import '@patternfly/react-topology/dist/esm/css/topology-controlbar.css'; -import '@patternfly/react-topology/dist/esm/css/topology-view.css'; +import { InstallSpecFormik } from '../../InstallWizard/types'; import './UpdateGraph.css'; -const ICON_SIZE = 12; - -type VersionNodeData = { - version: string; - channel: string; - isCurrentVersion: boolean; - isDeprecated: boolean; - entryName: string; -}; - -const NODE_DIAMETER = 20; - -type VersionNodeProps = { - element: Node; -} & WithSelectionProps; - -const VersionNodeComponent: React.FC = observer(({ element, selected, onSelect }) => { - const data = element.getData() as VersionNodeData; - - let nodeIcon: React.ReactNode = undefined; - if (data.isCurrentVersion) { - nodeIcon = ; - } else if (data.isDeprecated) { - nodeIcon = ; - } else if (selected) { - nodeIcon = ; - } - +const MoreVersions = ({ updates }: { updates: CatalogItemVersion[] }) => { + const { t } = useTranslation(); + const [, , { setValue }] = useField('version'); return ( - - - - {data.version} - - {nodeIcon && {nodeIcon}} - - + + { + return ( + + {updates.map((u) => ( + + ))} + + ); + }} + > + + + ); -}); - -const VersionNode = withSelection()(VersionNodeComponent); - -const customComponentFactory: ComponentFactory = (kind: ModelKind) => { - switch (kind) { - case ModelKind.graph: - return withPanZoom()(GraphComponent); - case ModelKind.node: - return VersionNode as never; - case ModelKind.edge: - return DefaultEdge; - default: - return undefined; - } }; -const customLayoutFactory: LayoutFactory = (type: string, graph: Graph): Layout | undefined => - new DagreLayout(graph, { - rankdir: LEFT_TO_RIGHT, - nodesep: 50, - ranksep: 80, - edgesep: 20, - }); - -const buildTopologyModel = ( - currentVersionEntry: CatalogItemVersion, - directUpgradeEntries: CatalogItemVersion[], - currentChannel: string, -): Model => { - const nodes: NodeModel[] = []; - const edges: EdgeModel[] = []; - - // Deduplicate entries - const entriesMap = new Map(); - directUpgradeEntries.forEach((entry) => entriesMap.set(entry.version, entry)); - if (currentVersionEntry) { - entriesMap.set(currentVersionEntry.version, currentVersionEntry); - } - const allEntries = Array.from(entriesMap.values()); - - // Map to track version nodes for edge creation - const versionToNodeId = new Map(); - - // Create version nodes - allEntries.forEach((versionEntry) => { - const versionName = versionEntry.version; - const nodeId = versionName; - - versionToNodeId.set(versionName, nodeId); +const Channel = ({ children }: React.PropsWithChildren) => { + return
{children}
; +}; - nodes.push({ - id: nodeId, - type: 'node', - label: versionName, - width: NODE_DIAMETER, - height: NODE_DIAMETER, - shape: NodeShape.ellipse, - data: { - version: versionName, - channel: currentChannel, - isCurrentVersion: versionName === currentVersionEntry.version, - isDeprecated: !!versionEntry.deprecation?.message, - entryName: versionName, - } as VersionNodeData, - }); - }); +const ChannelLine = ({ children }: React.PropsWithChildren) => { + return
  • {children}
  • ; +}; - const latestVersion = allEntries - .filter((e) => e.version !== currentVersionEntry.version) - .sort((a, b) => semver.rcompare(a.version, b.version))[0]?.version; +export const ChannelName = ({ children }: React.PropsWithChildren) => { + return {children}; +}; - const getEdgeStyle = (source: string, target: string) => - source === currentVersionEntry.version && target === latestVersion ? EdgeStyle.default : EdgeStyle.dashed; +const ChannelPath = ({ children }: React.PropsWithChildren) => { + return
      {children}
    ; +}; - // Create edges based on replaces, skips, and skipRange - allEntries.forEach((entry) => { - const targetNodeId = versionToNodeId.get(entry.version); - if (!targetNodeId) return; +const ChannelVersion = ({ children }: React.PropsWithChildren) => { + return {children}; +}; - // Edge from replaces (single version string) - if (entry.replaces) { - const sourceNodeId = versionToNodeId.get(entry.replaces); - if (sourceNodeId) { - edges.push({ - id: `edge-${sourceNodeId}-${targetNodeId}`, - type: 'edge', - source: sourceNodeId, - target: targetNodeId, - edgeStyle: getEdgeStyle(sourceNodeId, targetNodeId), - }); - } - } +const ChannelVersionDot = ({ version, current }: { version: string; current?: boolean }) => { + const { t } = useTranslation(); + const [{ value }, , { setValue }] = useField('version'); - // Edges from skips (array of version strings) - entry.skips?.forEach((skippedVersion) => { - const sourceNodeId = versionToNodeId.get(skippedVersion); - if (sourceNodeId) { - edges.push({ - id: `edge-skip-${sourceNodeId}-${targetNodeId}`, - type: 'edge', - source: sourceNodeId, - target: targetNodeId, - edgeStyle: getEdgeStyle(sourceNodeId, targetNodeId), - }); - } - }); + const isSelected = version === value; - // Edges from skipRange - find all versions in the graph that satisfy the range - if (entry.skipRange) { - allEntries.forEach((sourceEntry) => { - if (sourceEntry.version === entry.version) return; - if (semver.satisfies(sourceEntry.version, entry.skipRange!, { includePrerelease: true })) { - const sourceNodeId = versionToNodeId.get(sourceEntry.version); - if (sourceNodeId) { - edges.push({ - id: `edge-skiprange-${sourceNodeId}-${targetNodeId}`, - type: 'edge', - source: sourceNodeId, - target: targetNodeId, - edgeStyle: getEdgeStyle(sourceNodeId, targetNodeId), - }); - } - } - }); - } - }); + let className = 'fctl-channel-version-dot'; + let icon: React.ReactNode = undefined; + if (current) { + className = `${className} fctl-channel-version-dot--current`; + } else if (isSelected) { + className = `${className} fctl-channel-version-dot--selected`; + icon = ; + } - return { - nodes, - edges, - graph: { - id: 'update-graph', - type: 'graph', - layout: 'Dagre', - }, - }; + return ( + + )} + + + + + + {!!updates.length && ( + <> + + {values.version === currentVersion.version && ( + + - - - - - {!!updates.length && ( + )} + + + + + + )} - - { - onVersionChange?.(version); - setFieldValue('version', version); - }} - updates={updates} - /> - + {updateVersion?.deprecation && ( + + + {updateVersion.deprecation.message} + + + )} - )} - - + + + {showReadme && updateVersion?.readme && ( + setShowReadme(false)} variant="medium"> + + + {updateVersion.readme} + + + + )} + ) : ( diff --git a/libs/ui-components/src/components/Catalog/InstallWizard/InstallAppWizard.tsx b/libs/ui-components/src/components/Catalog/InstallWizard/InstallAppWizard.tsx index 854043b86..ab55600d8 100644 --- a/libs/ui-components/src/components/Catalog/InstallWizard/InstallAppWizard.tsx +++ b/libs/ui-components/src/components/Catalog/InstallWizard/InstallAppWizard.tsx @@ -92,7 +92,7 @@ const InstallAppWizardContent = ({ {currentStep?.id === appConfigStepId && }
    {values.target !== 'new-device' && ( diff --git a/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx b/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx index dc37c470a..abc36b139 100644 --- a/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx +++ b/libs/ui-components/src/components/Catalog/InstallWizard/InstallWizard.tsx @@ -60,7 +60,7 @@ const InstallWizard = () => { - {t('Install {{name}}', { name: catalogItem?.spec.displayName || itemId })} + {t('Deploy {{name}}', { name: catalogItem?.spec.displayName || itemId })} diff --git a/libs/ui-components/src/components/Catalog/InstallWizard/steps/ReviewStep.tsx b/libs/ui-components/src/components/Catalog/InstallWizard/steps/ReviewStep.tsx index 1e7b51c98..081e3564a 100644 --- a/libs/ui-components/src/components/Catalog/InstallWizard/steps/ReviewStep.tsx +++ b/libs/ui-components/src/components/Catalog/InstallWizard/steps/ReviewStep.tsx @@ -139,7 +139,7 @@ const ReviewStep = ({ error, catalogItem }: ReviewStepProps) => { - {t('Installation specifications')} + {t('Deployment specifications')} diff --git a/libs/ui-components/src/components/Catalog/InstallWizard/steps/SelectTargetStep.tsx b/libs/ui-components/src/components/Catalog/InstallWizard/steps/SelectTargetStep.tsx index b3166acd1..a8752133b 100644 --- a/libs/ui-components/src/components/Catalog/InstallWizard/steps/SelectTargetStep.tsx +++ b/libs/ui-components/src/components/Catalog/InstallWizard/steps/SelectTargetStep.tsx @@ -265,7 +265,7 @@ const NewDeviceTarget = ({ catalogItem }: NewDeviceTargetProps) => { - {t('Installation specifications')} + {t('Deployment specifications')} 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 3963a514f..70ef86e52 100644 --- a/libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx +++ b/libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx @@ -1,4 +1,4 @@ -import { CatalogItem } from '@flightctl/types/alpha'; +import { CatalogItem, CatalogItemVersion } from '@flightctl/types/alpha'; import { Alert, Button, @@ -35,6 +35,45 @@ import { applyInitialConfig, getInitialAppConfig } from '../utils'; import { InstallAppFormik } from '../types'; import WithTooltip from '../../../common/WithTooltip'; +type VersionDropdownProps = { + catalogItem: CatalogItem; + versions: CatalogItemVersion[]; +}; + +export const VersionDropdown = ({ catalogItem, versions }: VersionDropdownProps) => { + const { setFieldValue } = useFormikContext(); + const { t } = useTranslation(); + return ( + { + const appConfig = getInitialAppConfig(catalogItem, val); + applyInitialConfig(setFieldValue, appConfig); + }} + items={versions.reduce((acc, v) => { + return { + ...acc, + [v.version]: { + label: ( + + {v.version} + {v.deprecation && ( + + + + )} + + ), + selectedLabel: v.version, + }, + }; + }, {})} + /> + ); +}; + export const isSpecsStepValid = (errors: FormikErrors) => { return !errors.target && !errors.version && !errors.channel; }; @@ -95,33 +134,7 @@ export const InstallSpec = ({ - { - const appConfig = getInitialAppConfig(catalogItem, val); - applyInitialConfig(setFieldValue, appConfig); - }} - items={channelVersions.reduce((acc, v) => { - return { - ...acc, - [v.version]: { - label: ( - - {v.version} - {v.deprecation && ( - - - - )} - - ), - selectedLabel: v.version, - }, - }; - }, {})} - /> + {!hideReadmeLink && !!currentVersion?.readme && ( @@ -264,7 +277,7 @@ const SpecificationsStep = ({ catalogItem, showNewDevice }: SpecificationsStepPr name="target" checkedValue="fleet" label={{t('Existing Fleet')}} - description={t('Install to all devices in a fleet')} + description={t('Deploy to all devices in a fleet')} isDisabled={!!fleetDisabledReason} /> @@ -280,7 +293,7 @@ const SpecificationsStep = ({ catalogItem, showNewDevice }: SpecificationsStepPr name="target" checkedValue="device" label={{t('Existing Device')}} - description={t('Install to a single fleetless device')} + description={t('Deploy to a single fleetless device')} isDisabled={!!deviceDisabledReason} /> diff --git a/libs/ui-components/src/components/Catalog/InstalledSoftware.tsx b/libs/ui-components/src/components/Catalog/InstalledSoftware.tsx index dff6c732a..ca62ec5df 100644 --- a/libs/ui-components/src/components/Catalog/InstalledSoftware.tsx +++ b/libs/ui-components/src/components/Catalog/InstalledSoftware.tsx @@ -1,6 +1,6 @@ import { ContainerApplication, DeviceSpec } from '@flightctl/types'; import { ArrowCircleUpIcon } from '@patternfly/react-icons/dist/js/icons/arrow-circle-up-icon'; -import { ActionsColumn, IAction, Table, Tbody, Td, Tr } from '@patternfly/react-table'; +import { ActionsColumn, IAction } from '@patternfly/react-table'; import * as React from 'react'; import { CatalogItem, CatalogItemVersion } from '@flightctl/types/alpha'; import { @@ -10,6 +10,7 @@ import { CardTitle, Content, ContentVariants, + Divider, EmptyState, EmptyStateBody, Flex, @@ -37,48 +38,30 @@ import { } from './const'; import { useCatalogItem } from './useCatalogs'; -type UpdateColumnProps = { +type UpdateInfoProps = { catalogItem: CatalogItem; channel: string; catalogItemVersion: CatalogItemVersion; - appName: string | undefined; - onEditApp: VoidFunction; + onClick: VoidFunction; + canEdit: boolean; }; -const UpdateAppColumn = ({ catalogItem, channel, catalogItemVersion, onEditApp }: UpdateColumnProps) => { +const UpdateInfo = ({ onClick, catalogItem, channel, catalogItemVersion, canEdit }: UpdateInfoProps) => { const { t } = useTranslation(); const updates = getUpdates(catalogItem, channel, catalogItemVersion.version); - return ( - <> - {!!updates.length && ( - - )} - - ); -}; - -type UpdateOsColumnProps = { - catalogItem: CatalogItem; - channel: string; - catalogItemVersion: CatalogItemVersion; - onEditOs: VoidFunction; -}; - -const UpdateOsColumn = ({ onEditOs, catalogItem, channel, catalogItemVersion }: UpdateOsColumnProps) => { - const { t } = useTranslation(); - const updates = getUpdates(catalogItem, channel, catalogItemVersion.version); + if (!updates.length) { + return false; + } - return ( - <> - {!!updates.length && ( - - )} - + return canEdit ? ( + + ) : ( + ); }; @@ -223,15 +206,24 @@ const InstalledSoftware = ({ labels, spec, onDeleteOs, onDeleteApp, onEdit, canE {t('Select an operating system or application from the catalog below.')} ) : ( - - - {osItem && osCatalog && osChannel && catalogItemVersion && spec && ( - - - - - - - )} - {appItems?.map((app) => { - const appChannel = labels?.[`${app.name}.${APP_CHANNEL_LABEL_KEY}`] || ''; - const appSpec = spec?.applications?.find((a) => a.name === app.name); - const itemVersion = - appSpec && - app.item.spec.versions.find((v) => { - const refUri = getFullReferenceURI(app.item.spec.reference.uri, v); - const imageMatches = refUri === (appSpec as ContainerApplication).image; - return imageMatches && v.channels.includes(appChannel); - }); - const actions: IAction[] = [ - ...(itemVersion - ? [ - { - title: t('Edit'), - onClick: () => onEdit(app.item.metadata.catalog, app.item.metadata.name || '', app.name), - }, - ] - : []), - { - title: t('Delete'), - onClick: () => setAppToDelete(app.name), - }, - ]; + + )} + + + )} + {appItems?.map((app, index) => { + const appChannel = labels?.[`${app.name}.${APP_CHANNEL_LABEL_KEY}`] || ''; + const appSpec = spec?.applications?.find((a) => a.name === app.name); + const itemVersion = + appSpec && + app.item.spec.versions.find((v) => { + const refUri = getFullReferenceURI(app.item.spec.reference.uri, v); + const imageMatches = refUri === (appSpec as ContainerApplication).image; + return imageMatches && v.channels.includes(appChannel); + }); + const actions: IAction[] = [ + ...(itemVersion + ? [ + { + title: t('Edit'), + onClick: () => onEdit(app.item.metadata.catalog, app.item.metadata.name || '', app.name), + }, + ] + : []), + { + title: t('Delete'), + onClick: () => setAppToDelete(app.name), + }, + ]; - return ( - - - - - - - - - ); - })} - -
    + + {osItem && osCatalog && osChannel && catalogItemVersion && spec && ( + + + - - {(osItem.spec.deprecation || catalogItemVersion.deprecation) && ( + + + onEdit(osItem.metadata.catalog, osItem.metadata.name || '')} + canEdit={canEdit} + /> + + {(osItem.spec.deprecation || catalogItemVersion.deprecation) && ( + - )} - - onEdit(osItem.metadata.catalog, osItem.metadata.name || '')} - /> - - {canEdit && ( + + )} + {canEdit && ( + - )} -
    + return ( + + {(hasOs || index > 0) && } + + + - - {(app.item.spec.deprecation || itemVersion?.deprecation) && ( + + + {itemVersion && ( + onEdit(app.item.metadata.catalog, app.item.metadata.name || '', app.name)} + canEdit={canEdit} + /> + )} + + {(app.item.spec.deprecation || itemVersion?.deprecation) && ( + - )} - - {itemVersion && canEdit && spec && ( - - onEdit(app.item.metadata.catalog, app.item.metadata.name || '', app.name) - } - /> - )} - {canEdit && }
    + + )} + {canEdit && ( + + + + )} + +
    + + ); + })} +
    )}
    diff --git a/package-lock.json b/package-lock.json index 34c912c59..c39ce2871 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,6 @@ "@patternfly/react-icons": "^6.4.0", "@patternfly/react-styles": "^6.4.0", "@patternfly/react-table": "^6.4.0", - "@patternfly/react-topology": "^6.4.0", "@types/react-redux": "^7.1.33", "formik": "^2.4.9", "fuzzysearch": "^1.0.3", @@ -265,7 +264,6 @@ "@patternfly/react-icons": "^6.4.0", "@patternfly/react-styles": "^6.4.0", "@patternfly/react-table": "^6.4.0", - "@patternfly/react-topology": "^6.4.0", "formik": "^2.4.5", "fuzzysearch": "^1.0.3", "i18next": "^21.8.14", @@ -450,7 +448,6 @@ "@patternfly/react-icons": "^6.4.0", "@patternfly/react-styles": "^6.4.0", "@patternfly/react-table": "^6.4.0", - "@patternfly/react-topology": "^6.4.0", "i18next": "21.8.14 - 23.x", "monaco-editor": "^0.51.0", "react": "17.0.1 - 18.x", From cf29e644fe6eb635cb16af3bb832b443c3e9b5c2 Mon Sep 17 00:00:00 2001 From: rawagner Date: Wed, 4 Mar 2026 17:36:31 +0100 Subject: [PATCH 2/2] address review comments --- libs/i18n/locales/en/translation.json | 2 ++ .../Catalog/EditWizard/steps/UpdateGraph.css | 6 ++--- .../Catalog/EditWizard/steps/UpdateGraph.tsx | 4 ++-- .../Catalog/EditWizard/steps/UpdateStep.tsx | 2 ++ .../steps/SpecificationsStep.tsx | 24 +++++++++++++------ 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 9faf05a2e..73307ae03 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -242,6 +242,7 @@ "Other available paths": "Other available paths", "+ More": "+ More", "Version {{version}}": "Version {{version}}", + "{{version}} (current)": "{{version}} (current)", "{{currentChannel}} channel": "{{currentChannel}} channel", "Version update": "Version update", "Current channel": "Current channel", @@ -318,6 +319,7 @@ "Deploy to all devices in a fleet": "Deploy to all devices in a fleet", "Existing Device": "Existing Device", "Deploy to a single fleetless device": "Deploy to a single fleetless device", + "This Operating system does not contain any additional formats besides bootc": "This Operating system does not contain any additional formats besides bootc", "New Device": "New Device", "Provision a brand new, unenrolled device": "Provision a brand new, unenrolled device", "Update configuration successful": "Update configuration successful", diff --git a/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.css b/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.css index fcc248735..1f11f2081 100644 --- a/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.css +++ b/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.css @@ -100,12 +100,12 @@ .fctl-channel-version-dot { background: var(--pf-t--global--color--brand--default) !important; - border-radius: 16px !important; - height: 16px !important; + border-radius: 18px !important; + height: 18px !important; padding: 0 !important; position: absolute; min-width: auto; - width: 16px !important; + width: 18px !important; z-index: 2; } diff --git a/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx b/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx index e36f81129..3fda6d6bc 100644 --- a/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx +++ b/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateGraph.tsx @@ -36,7 +36,7 @@ const MoreVersions = ({ updates }: { updates: CatalogItemVersion[] }) => { ); }} > - @@ -152,7 +152,7 @@ const UpdateGraph = ({ currentChannel, updates, currentVersion }: UpdateGraphPro - {`${currentVersion.version} (current)`} + {t('{{version}} (current)', { version: currentVersion.version })} {content} diff --git a/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateStep.tsx b/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateStep.tsx index a72f52429..d4acc472c 100644 --- a/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateStep.tsx +++ b/libs/ui-components/src/components/Catalog/EditWizard/steps/UpdateStep.tsx @@ -8,6 +8,7 @@ import { Icon, Modal, ModalBody, + ModalHeader, Title, } from '@patternfly/react-core'; import ArrowRightIcon from '@patternfly/react-icons/dist/js/icons/arrow-right-icon'; @@ -157,6 +158,7 @@ const UpdateStep = ({ currentVersion, catalogItem, isEdit }: UpdateStepProps) => {showReadme && updateVersion?.readme && ( setShowReadme(false)} variant="medium"> + {updateVersion.readme} 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 70ef86e52..f16ff21ae 100644 --- a/libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx +++ b/libs/ui-components/src/components/Catalog/InstallWizard/steps/SpecificationsStep.tsx @@ -10,6 +10,7 @@ import { Label, Modal, ModalBody, + ModalHeader, Spinner, Split, SplitItem, @@ -154,6 +155,7 @@ export const InstallSpec = ({ {showReadme && currentVersion?.readme && ( setShowReadme(false)} variant="medium"> + {currentVersion.readme} @@ -227,6 +229,7 @@ const SpecificationsStep = ({ catalogItem, showNewDevice }: SpecificationsStepPr const [canEditFleet, canListFleet, canEditDevice, canListDevice] = checkPermissions(targetPermissions); const fleetRadioRef = React.useRef(null); const deviceRadioRef = React.useRef(null); + const newDeviceRadioRef = React.useRef(null); const { fleets, isLoading: fleetsLoading } = useFleets({}); const { devices, isLoading: devicesLoading } = useDevicesPaginated({ @@ -300,13 +303,20 @@ const SpecificationsStep = ({ catalogItem, showNewDevice }: SpecificationsStepPr
    {showNewDevice && ( - + + {t('New Device')}} + description={t('Provision a brand new, unenrolled device')} + isDisabled={!catalogItem.spec.reference.artifacts?.length} + /> + )}