diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 9c158f3db..86eb9120e 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -1274,10 +1274,12 @@ "Login": "Login", "No {{ productName }} command line tools were found for this deployment at this time.": "No {{ productName }} command line tools were found for this deployment at this time.", "Could not list the {{ productName }} command line tools": "Could not list the {{ productName }} command line tools", - "Download flightctl CLI for {{ os }} for {{ arch }}": "Download flightctl CLI for {{ os }} for {{ arch }}", + "flightctl Command Line Interface (CLI)": "flightctl Command Line Interface (CLI)", + "flightctl is the command-line interface for managing {{ productName }} fleets, devices, and workloads.": "flightctl is the command-line interface for managing {{ productName }} fleets, devices, and workloads.", + "flightctl-restore Command Line Interface (CLI)": "flightctl-restore Command Line Interface (CLI)", + "flightctl-restore prepares devices after database restoration. Use when restoring {{ productName }} from backup.": "flightctl-restore prepares devices after database restoration. Use when restoring {{ productName }} from backup.", "Red Hat Edge Manager": "Red Hat Edge Manager", "Flight Control": "Flight Control", - "With the {{ productName }} command line interface, you can manage your fleets, devices and repositories from a terminal.": "With the {{ productName }} command line interface, you can manage your fleets, devices and repositories from a terminal.", "Command line tools are not available for download in this {{ productName }} installation.": "Command line tools are not available for download in this {{ productName }} installation.", "System default": "System default", "Light": "Light", @@ -1555,6 +1557,9 @@ "OpenShift": "OpenShift", "Kubernetes": "Kubernetes", "Ansible Automation Platform": "Ansible Automation Platform", + "Download flightctl for Mac ({{ arch }})": "Download flightctl for Mac ({{ arch }})", + "Download flightctl for Linux ({{ arch }})": "Download flightctl for Linux ({{ arch }})", + "Download flightctl for Windows ({{ arch }})": "Download flightctl for Windows ({{ arch }})", "{{count}} years ago_one": "{{count}} year ago", "{{count}} years ago_other": "{{count}} years ago", "{{count}} months ago_one": "{{count}} month ago", diff --git a/libs/ui-components/src/components/Masthead/CommandLineToolsPage.tsx b/libs/ui-components/src/components/Masthead/CommandLineToolsPage.tsx index 981ce9f39..a314c7b91 100644 --- a/libs/ui-components/src/components/Masthead/CommandLineToolsPage.tsx +++ b/libs/ui-components/src/components/Masthead/CommandLineToolsPage.tsx @@ -15,21 +15,43 @@ import ExternalLinkAltIcon from '@patternfly/react-icons/dist/js/icons/external- import { useTranslation } from '../../hooks/useTranslation'; import { useAppContext } from '../../hooks/useAppContext'; -import { getErrorMessage } from '../../utils/error'; -import { CliArtifactsResponse } from '../../types/extraTypes'; +import { CliArtifactsDisplayResponse, useCliArtifacts } from '../../hooks/useCliArtifacts'; +import { CliArtifact } from '../../types/extraTypes'; +import { getArtifactDownloadLabel, getArtifactUrl } from '../../utils/cliArtifacts'; type CommandLineToolsContentProps = { productName: string; loading: boolean; loadError?: string; - artifactsResponse?: CliArtifactsResponse; + artifactsResponse?: CliArtifactsDisplayResponse; }; -type CommandLineArtifact = CliArtifactsResponse['artifacts'][0]; - -const getArtifactUrl = (baseUrl: string, artifact: CommandLineArtifact) => { - const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; - return `${normalizedBaseUrl}/${artifact.arch}/${artifact.os}/${artifact.filename}`; +const ArtifactDownloadList = ({ baseUrl, items }: { baseUrl: string; items: CliArtifact[] }) => { + const { t } = useTranslation(); + return ( + + {items.map((cliArtifact) => { + const linkText = getArtifactDownloadLabel(cliArtifact, t); + return ( + + + + ); + })} + + ); }; const CommandLineToolsContent = ({ @@ -40,14 +62,13 @@ const CommandLineToolsContent = ({ }: CommandLineToolsContentProps) => { const { t } = useTranslation(); - if (loading) { + if (loading || !artifactsResponse) { return ; } let errorMessage = loadError; - const cliArtifacts = artifactsResponse?.artifacts || []; - if (cliArtifacts.length === 0) { + if (artifactsResponse.totalCount === 0) { errorMessage = t('No {{ productName }} command line tools were found for this deployment at this time.', { productName, }); @@ -65,70 +86,50 @@ const CommandLineToolsContent = ({ ); } + const { baseUrl, flightctlArtifacts, restoreArtifacts } = artifactsResponse; + return ( - - {cliArtifacts.map((cliArtifact) => { - const linkText = t('Download flightctl CLI for {{ os }} for {{ arch }}', { - os: cliArtifact.os, - arch: cliArtifact.arch, - }); - return ( - - - - ); - })} - + + {flightctlArtifacts.length > 0 ? ( + <> + + {t('flightctl Command Line Interface (CLI)')} + + + {t( + 'flightctl is the command-line interface for managing {{ productName }} fleets, devices, and workloads.', + { productName }, + )} + + + + + + ) : null} + {restoreArtifacts.length > 0 ? ( + <> + + {t('flightctl-restore Command Line Interface (CLI)')} + + + {t( + 'flightctl-restore prepares devices after database restoration. Use when restoring {{ productName }} from backup.', + { productName }, + )} + + + + + + ) : null} + ); }; const CommandLineToolsPage = () => { const { t } = useTranslation(); - const { fetch, settings } = useAppContext(); - const proxyFetch = fetch.proxyFetch; - - const [loading, setLoading] = React.useState(true); - const [loadError, setLoadError] = React.useState(); - const [artifactsResponse, setCliArtifactsResponse] = React.useState(); - const [hasArtifactsEnabled, setArtifactsEnabled] = React.useState(true); - - React.useEffect(() => { - const getLinks = async () => { - try { - const response = await proxyFetch('cli-artifacts', { - method: 'GET', - }); - if (!response.ok) { - if (response.status === 501) { - // Response that indicatest that the feature is disabled - setArtifactsEnabled(false); - } else { - setLoadError(getErrorMessage(response.statusText)); - } - return; - } - const apiResponse = (await response.json()) as CliArtifactsResponse; - setCliArtifactsResponse(apiResponse); - } catch { - setArtifactsEnabled(false); - } finally { - setLoading(false); - } - }; - void getLinks(); - }, [proxyFetch]); + const { settings } = useAppContext(); + const { loading, loadError, hasArtifactsEnabled, artifactsResponse } = useCliArtifacts(); const productName = settings.isRHEM ? t('Red Hat Edge Manager') : t('Flight Control'); @@ -139,14 +140,6 @@ const CommandLineToolsPage = () => { {t('Command Line Tools')} - - {t( - 'With the {{ productName }} command line interface, you can manage your fleets, devices and repositories from a terminal.', - { - productName, - }, - )} - {hasArtifactsEnabled ? ( { + const { fetch } = useAppContext(); + const { proxyFetch } = fetch; + + const [loading, setLoading] = React.useState(true); + const [loadError, setLoadError] = React.useState(); + const [artifactsResponse, setArtifactsResponse] = React.useState(); + const [hasArtifactsEnabled, setArtifactsEnabled] = React.useState(true); + + React.useEffect(() => { + const getLinks = async () => { + try { + const response = await proxyFetch('cli-artifacts', { + method: 'GET', + }); + if (!response.ok) { + if (response.status === 501) { + setArtifactsEnabled(false); + } else { + setLoadError(getErrorMessage(response.statusText)); + } + return; + } + const apiResponse = (await response.json()) as CliArtifactsResponse; + const artifacts = sortCliArtifacts(apiResponse.artifacts); + const mainCliArtifacts = artifacts.filter((a) => getArtifactTool(a) === CliArtifactTool.Flightctl); + const restoreCliArtifacts = artifacts.filter((a) => getArtifactTool(a) === CliArtifactTool.FlightctlRestore); + setArtifactsResponse({ + baseUrl: apiResponse.baseUrl, + totalCount: artifacts.length, + flightctlArtifacts: mainCliArtifacts, + restoreArtifacts: restoreCliArtifacts, + }); + } catch { + setArtifactsEnabled(false); + } finally { + setLoading(false); + } + }; + void getLinks(); + }, [proxyFetch]); + + return { + loading, + loadError, + hasArtifactsEnabled, + artifactsResponse, + }; +}; diff --git a/libs/ui-components/src/types/extraTypes.ts b/libs/ui-components/src/types/extraTypes.ts index 5cb87d862..f02143088 100644 --- a/libs/ui-components/src/types/extraTypes.ts +++ b/libs/ui-components/src/types/extraTypes.ts @@ -55,11 +55,17 @@ export const isFleet = (resource: ResourceSync | Fleet): resource is Fleet => re // We use the fixed type to get proper Typescript checks for the field export type InlineApplicationFileFixed = FileContent & RelativePath; -type CliArtifact = { - os: string; - arch: string; +export enum CliArtifactTool { + Flightctl = 'flightctl', + FlightctlRestore = 'flightctl-restore', +} + +export type CliArtifact = { + os: 'linux' | 'mac' | 'windows'; + arch: 'amd64' | 'arm64'; filename: string; sha256: string; + tool?: CliArtifactTool; }; export type CliArtifactsResponse = { diff --git a/libs/ui-components/src/utils/cliArtifacts.ts b/libs/ui-components/src/utils/cliArtifacts.ts new file mode 100644 index 000000000..a50473cd5 --- /dev/null +++ b/libs/ui-components/src/utils/cliArtifacts.ts @@ -0,0 +1,67 @@ +import { TFunction } from 'i18next'; + +import { CliArtifact, CliArtifactTool } from '../types/extraTypes'; + +// The OpenShift Console sorts links alphabetically using case-sensitive comparison. +// To match the same ordering, we define the sorting here. +const OS_SORT_ORDER: Record = { + linux: 0, + mac: 1, + windows: 2, +}; + +const ARCH_SORT_ORDER: Record = { + arm64: 0, + amd64: 1, +}; + +export const sortCliArtifacts = (artifacts: CliArtifact[]): CliArtifact[] => + [...artifacts].sort((a, b) => { + const osA = OS_SORT_ORDER[a.os] ?? 99; + const osB = OS_SORT_ORDER[b.os] ?? 99; + if (osA !== osB) { + return osA - osB; + } + const archA = ARCH_SORT_ORDER[a.arch] ?? 99; + const archB = ARCH_SORT_ORDER[b.arch] ?? 99; + return archA - archB; + }); + +export const getArtifactUrl = (baseUrl: string, artifact: CliArtifact) => { + const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; + return `${normalizedBaseUrl}/${artifact.arch}/${artifact.os}/${artifact.filename}`; +}; + +export const getArtifactTool = (artifact: CliArtifact) => { + if (!artifact.tool) { + // Handle backward compatibility with older artifacts before "tool" was added + return artifact.filename.includes('flightctl-restore') + ? CliArtifactTool.FlightctlRestore + : CliArtifactTool.Flightctl; + } + return artifact.tool; +}; + +export const getArchLabel = (arch: string): string => { + switch (arch) { + case 'amd64': + return 'x86_64'; + case 'arm64': + return 'ARM64'; + default: + return arch; + } +}; + +export const getArtifactDownloadLabel = (artifact: CliArtifact, t: TFunction): string => { + const archLabel = getArchLabel(artifact.arch); + + switch (artifact.os) { + case 'mac': + return t('Download flightctl for Mac ({{ arch }})', { arch: archLabel }); + case 'linux': + return t('Download flightctl for Linux ({{ arch }})', { arch: archLabel }); + case 'windows': + return t('Download flightctl for Windows ({{ arch }})', { arch: archLabel }); + } +};