Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions libs/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
147 changes: 70 additions & 77 deletions libs/ui-components/src/components/Masthead/CommandLineToolsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<List>
{items.map((cliArtifact) => {
const linkText = getArtifactDownloadLabel(cliArtifact, t);
return (
<ListItem key={cliArtifact.filename}>
<Button
component="a"
variant="link"
isInline
href={getArtifactUrl(baseUrl, cliArtifact)}
target="_blank"
rel="noopener noreferrer"
icon={<ExternalLinkAltIcon />}
iconPosition="end"
aria-label={linkText}
>
{linkText}
</Button>
</ListItem>
);
})}
</List>
);
};

const CommandLineToolsContent = ({
Expand All @@ -40,14 +62,13 @@ const CommandLineToolsContent = ({
}: CommandLineToolsContentProps) => {
const { t } = useTranslation();

if (loading) {
if (loading || !artifactsResponse) {
return <Spinner size="sm" />;
}

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,
});
Expand All @@ -65,70 +86,50 @@ const CommandLineToolsContent = ({
);
}

const { baseUrl, flightctlArtifacts, restoreArtifacts } = artifactsResponse;

return (
<List>
{cliArtifacts.map((cliArtifact) => {
const linkText = t('Download flightctl CLI for {{ os }} for {{ arch }}', {
os: cliArtifact.os,
arch: cliArtifact.arch,
});
return (
<ListItem key={cliArtifact.filename}>
<Button
component="a"
variant="link"
isInline
href={getArtifactUrl(artifactsResponse?.baseUrl || '', cliArtifact)}
target="_blank"
rel="noopener noreferrer"
icon={<ExternalLinkAltIcon />}
iconPosition="end"
aria-label={linkText}
>
{linkText}
</Button>
</ListItem>
);
})}
</List>
<Stack hasGutter>
{flightctlArtifacts.length > 0 ? (
<>
<StackItem>
<Title headingLevel="h2">{t('flightctl Command Line Interface (CLI)')}</Title>
</StackItem>
<StackItem>
{t(
'flightctl is the command-line interface for managing {{ productName }} fleets, devices, and workloads.',
{ productName },
)}
</StackItem>
<StackItem>
<ArtifactDownloadList baseUrl={baseUrl} items={flightctlArtifacts} />
</StackItem>
</>
) : null}
{restoreArtifacts.length > 0 ? (
<>
<StackItem>
<Title headingLevel="h2">{t('flightctl-restore Command Line Interface (CLI)')}</Title>
</StackItem>
<StackItem>
{t(
'flightctl-restore prepares devices after database restoration. Use when restoring {{ productName }} from backup.',
{ productName },
)}
</StackItem>
<StackItem>
<ArtifactDownloadList baseUrl={baseUrl} items={restoreArtifacts} />
</StackItem>
</>
) : null}
</Stack>
);
};

const CommandLineToolsPage = () => {
const { t } = useTranslation();
const { fetch, settings } = useAppContext();
const proxyFetch = fetch.proxyFetch;

const [loading, setLoading] = React.useState<boolean>(true);
const [loadError, setLoadError] = React.useState<string>();
const [artifactsResponse, setCliArtifactsResponse] = React.useState<CliArtifactsResponse>();
const [hasArtifactsEnabled, setArtifactsEnabled] = React.useState<boolean>(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');

Expand All @@ -139,14 +140,6 @@ const CommandLineToolsPage = () => {
<Title headingLevel="h1">{t('Command Line Tools')}</Title>
</StackItem>
<Divider />
<StackItem>
{t(
'With the {{ productName }} command line interface, you can manage your fleets, devices and repositories from a terminal.',
{
productName,
},
)}
</StackItem>
{hasArtifactsEnabled ? (
<StackItem>
<CommandLineToolsContent
Expand Down
68 changes: 68 additions & 0 deletions libs/ui-components/src/hooks/useCliArtifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as React from 'react';

import { useAppContext } from './useAppContext';
import { getErrorMessage } from '../utils/error';
import { getArtifactTool, sortCliArtifacts } from '../utils/cliArtifacts';
import { CliArtifact, CliArtifactTool, CliArtifactsResponse } from '../types/extraTypes';

export type CliArtifactsDisplayResponse = {
baseUrl: string;
totalCount: number;
flightctlArtifacts: CliArtifact[];
restoreArtifacts: CliArtifact[];
};

export const useCliArtifacts = (): {
loading: boolean;
loadError?: string;
hasArtifactsEnabled: boolean;
artifactsResponse?: CliArtifactsDisplayResponse;
} => {
const { fetch } = useAppContext();
const { proxyFetch } = fetch;

const [loading, setLoading] = React.useState(true);
const [loadError, setLoadError] = React.useState<string>();
const [artifactsResponse, setArtifactsResponse] = React.useState<CliArtifactsDisplayResponse>();
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,
};
};
12 changes: 9 additions & 3 deletions libs/ui-components/src/types/extraTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
67 changes: 67 additions & 0 deletions libs/ui-components/src/utils/cliArtifacts.ts
Original file line number Diff line number Diff line change
@@ -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<string, number> = {
linux: 0,
mac: 1,
windows: 2,
};

const ARCH_SORT_ORDER: Record<string, number> = {
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 });
}
};
Loading