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 (
+
+ }
+ iconPosition="end"
+ aria-label={linkText}
+ >
+ {linkText}
+
+
+ );
+ })}
+
+ );
};
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 (
-
- }
- iconPosition="end"
- aria-label={linkText}
- >
- {linkText}
-
-
- );
- })}
-
+
+ {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 });
+ }
+};