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
1 change: 1 addition & 0 deletions libs/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,7 @@
"Image builds cannot be edited. Use Retry to create a new image build based on this one.": "Image builds cannot be edited. Use Retry to create a new image build based on this one.",
"Date": "Date",
"Build failed. Please retry.": "Build failed. Please retry.",
"Build failed.": "Build failed.",
"View more": "View more",
"There are no image builds in your environment.": "There are no image builds in your environment.",
"Generate system images for consistent deployment to edge devices.": "Generate system images for consistent deployment to edge devices.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import ImageBuildLogsTab from './ImageBuildLogsTab';
const imageBuildDetailsPermissions = [
{ kind: RESOURCE.IMAGE_BUILD, verb: VERB.CREATE },
{ kind: RESOURCE.IMAGE_BUILD, verb: VERB.DELETE },
// Users that can view logs for imagebuilds also can view logs for imageexports
{ kind: RESOURCE.IMAGE_BUILD_LOG, verb: VERB.GET },
];

const ImageBuildDetailsPageContent = () => {
Expand All @@ -36,9 +38,14 @@ const ImageBuildDetailsPageContent = () => {
const [imageBuild, isLoading, error, refetch] = useImageBuild(imageBuildId);
const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState<boolean>();
const { checkPermissions } = usePermissionsContext();
const [canCreate, canDelete] = checkPermissions(imageBuildDetailsPermissions);
const [canCreate, canDelete, canViewLogs] = checkPermissions(imageBuildDetailsPermissions);
const buildReason = imageBuild ? getImageBuildStatusReason(imageBuild) : undefined;

const tabKeys = React.useMemo(
() => (canViewLogs ? ['details', 'exports', 'yaml', 'logs'] : ['details', 'exports', 'yaml']),
[canViewLogs],
);

return (
<DetailsPage
loading={isLoading}
Expand All @@ -48,11 +55,11 @@ const ImageBuildDetailsPageContent = () => {
resourceType="Image builds"
resourceTypeLabel={t('Image builds')}
nav={
<TabsNav aria-label="Image build details tabs" tabKeys={['details', 'exports', 'yaml', 'logs']}>
<TabsNav aria-label="Image build details tabs" tabKeys={tabKeys}>
<Tab eventKey="details" title={t('Image details')} />
<Tab eventKey="exports" title={t('Export images')} />
<Tab eventKey="yaml" title={t('YAML')} />
<Tab eventKey="logs" title={t('Logs')} />
{canViewLogs && <Tab eventKey="logs" title={t('Logs')} />}
</TabsNav>
}
actions={
Expand Down Expand Up @@ -81,7 +88,7 @@ const ImageBuildDetailsPageContent = () => {
<Route path="details" element={<ImageBuildDetailsTab imageBuild={imageBuild} />} />
<Route path="exports" element={<ImageBuildExportsGallery imageBuild={imageBuild} refetch={refetch} />} />
<Route path="yaml" element={<ImageBuildYaml imageBuild={imageBuild} refetch={refetch} />} />
<Route path="logs" element={<ImageBuildLogsTab imageBuild={imageBuild} />} />
{canViewLogs && <Route path="logs" element={<ImageBuildLogsTab imageBuild={imageBuild} />} />}
</Routes>
{isDeleteModalOpen && (
<DeleteImageBuildModal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { saveAs } from 'file-saver';

import { ExportFormatType, ImageExport } from '@flightctl/types/imagebuilder';
import { ImageBuildWithExports } from '../../../types/extraTypes';
import { RESOURCE, VERB } from '../../../types/rbac';
import { useFetch } from '../../../hooks/useFetch';
import { usePermissionsContext } from '../../common/PermissionsContext';
import { getErrorMessage } from '../../../utils/error';
import { getImageExportResource } from '../CreateImageBuildWizard/utils';
import { ViewImageBuildExportCard } from '../ImageExportCards';
Expand All @@ -30,8 +32,15 @@ const createDownloadLink = (url: string) => {
document.body.removeChild(link);
};

const imageBuildExportsPermissions = [
{ kind: RESOURCE.IMAGE_EXPORT, verb: VERB.CREATE },
{ kind: RESOURCE.IMAGE_EXPORT_DOWNLOAD, verb: VERB.GET },
];

const ImageBuildExportsGallery = ({ imageBuild, refetch }: ImageBuildExportsGalleryProps) => {
const { post, proxyFetch } = useFetch();
const { checkPermissions } = usePermissionsContext();
const [canCreateExport, canDownload] = checkPermissions(imageBuildExportsPermissions);
const [error, setError] = React.useState<{
format: ExportFormatType;
message: string;
Expand Down Expand Up @@ -117,8 +126,8 @@ const ImageBuildExportsGallery = ({ imageBuild, refetch }: ImageBuildExportsGall
isDownloading={downloadingFormat === format}
isDisabled={isDisabled}
onDismissError={() => setError(undefined)}
onExportImage={handleExportImage}
onDownload={handleDownload}
onExportImage={canCreateExport ? handleExportImage : undefined}
onDownload={canDownload ? handleDownload : undefined}
/>
);
})}
Expand Down
46 changes: 29 additions & 17 deletions libs/ui-components/src/components/ImageBuilds/ImageBuildRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type ImageBuildRowProps = {
rowIndex: number;
onRowSelect: (imageBuild: ImageBuild) => OnSelect;
isRowSelected: (imageBuild: ImageBuild) => boolean;
canCreate: boolean;
canDelete: boolean;
onDeleteClick: VoidFunction;
refetch: VoidFunction;
Expand All @@ -29,6 +30,7 @@ const ImageBuildRow = ({
onRowSelect,
isRowSelected,
onDeleteClick,
canCreate,
canDelete,
refetch,
}: ImageBuildRowProps) => {
Expand All @@ -48,12 +50,14 @@ const ImageBuildRow = ({
},
];

actions.push({
title: buildReason === ImageBuildConditionReason.ImageBuildConditionReasonFailed ? t('Retry') : t('Duplicate'),
onClick: () => {
navigate({ route: ROUTE.IMAGE_BUILD_EDIT, postfix: imageBuildName });
},
});
if (canCreate) {
actions.push({
title: buildReason === ImageBuildConditionReason.ImageBuildConditionReasonFailed ? t('Retry') : t('Duplicate'),
onClick: () => {
navigate({ route: ROUTE.IMAGE_BUILD_EDIT, postfix: imageBuildName });
},
});
}

if (canDelete) {
actions.push({
Expand Down Expand Up @@ -114,17 +118,25 @@ const ImageBuildRow = ({
<ExclamationCircleIcon />
</Icon>
</FlexItem>
<FlexItem>
<Content>{t('Build failed. Please retry.')}</Content>
</FlexItem>
<FlexItem>
<Button
variant="link"
onClick={() => navigate({ route: ROUTE.IMAGE_BUILD_EDIT, postfix: imageBuildName })}
>
{t('Retry')}
</Button>
</FlexItem>
{canCreate ? (
<>
<FlexItem>
<Content>{t('Build failed. Please retry.')}</Content>
</FlexItem>
<FlexItem>
<Button
variant="link"
onClick={() => navigate({ route: ROUTE.IMAGE_BUILD_EDIT, postfix: imageBuildName })}
>
{t('Retry')}
</Button>
</FlexItem>
</>
) : (
<FlexItem>
<Content>{t('Build failed.')}</Content>
</FlexItem>
)}
</Flex>
)}
<StackItem>
Expand Down
23 changes: 14 additions & 9 deletions libs/ui-components/src/components/ImageBuilds/ImageBuildsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,20 @@ const imageBuildTablePermissions = [
{ kind: RESOURCE.IMAGE_BUILD, verb: VERB.DELETE },
];

const ImageBuildsEmptyState = ({ onCreateClick }: { onCreateClick: () => void }) => {
const ImageBuildsEmptyState = ({ onCreateClick }: { onCreateClick?: VoidFunction }) => {
const { t } = useTranslation();
return (
<ResourceListEmptyState icon={PlusCircleIcon} titleText={t('There are no image builds in your environment.')}>
<EmptyStateBody>{t('Generate system images for consistent deployment to edge devices.')}</EmptyStateBody>
<EmptyStateFooter>
<EmptyStateActions>
<Button variant="primary" onClick={onCreateClick} icon={<PlusIcon />}>
{t('Build new image')}
</Button>
</EmptyStateActions>
</EmptyStateFooter>
{onCreateClick && (
<EmptyStateFooter>
<EmptyStateActions>
<Button variant="primary" onClick={onCreateClick} icon={<PlusIcon />}>
{t('Build new image')}
</Button>
</EmptyStateActions>
</EmptyStateFooter>
)}
</ResourceListEmptyState>
);
};
Expand Down Expand Up @@ -138,6 +140,7 @@ const ImageBuildTable = () => {
key={name}
imageBuild={imageBuild}
rowIndex={rowIndex}
canCreate={canCreate}
canDelete={canDelete}
onDeleteClick={() => {
setImageBuildToDeleteId(name);
Expand All @@ -150,7 +153,9 @@ const ImageBuildTable = () => {
})}
</Table>
<TablePagination pagination={pagination} isUpdating={isUpdating} />
{!isUpdating && imageBuilds.length === 0 && !name && <ImageBuildsEmptyState onCreateClick={handleCreateClick} />}
{!isUpdating && imageBuilds.length === 0 && !name && (
<ImageBuildsEmptyState onCreateClick={canCreate ? handleCreateClick : undefined} />
)}

{imageBuildToDeleteId && (
<DeleteImageBuildModal
Expand Down
56 changes: 29 additions & 27 deletions libs/ui-components/src/components/ImageBuilds/ImageExportCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ import { BuilderImageIcon } from '@patternfly/react-icons/dist/js/icons/builder-
import { ExportFormatType, ImageExport, ImageExportConditionReason } from '@flightctl/types/imagebuilder';
import { getExportFormatDescription, getExportFormatLabel, getImageExportStatusReason } from '../../utils/imageBuilds';
import { getDateDisplay } from '../../utils/dates';
import { usePermissionsContext } from '../common/PermissionsContext';
import { RESOURCE, VERB } from '../../types/rbac';
import { useTranslation } from '../../hooks/useTranslation';
import { ImageExportStatusDisplay } from './ImageBuildAndExportStatus';
import { useAppContext } from '../../hooks/useAppContext';
import { ROUTE } from '../../hooks/useNavigate';
import { ImageExportStatusDisplay } from './ImageBuildAndExportStatus';

import './ImageExportCards.css';

Expand All @@ -44,8 +46,8 @@ export type ImageExportFormatCardProps = {
format: ExportFormatType;
error?: { message: string; mode: 'export' | 'download' } | null;
imageExport?: ImageExport;
onExportImage: (format: ExportFormatType) => void;
onDownload: (format: ExportFormatType) => void;
onExportImage?: (format: ExportFormatType) => void;
onDownload?: (format: ExportFormatType) => void;
onDismissError: VoidFunction;
isCreating: boolean;
isDownloading?: boolean;
Expand All @@ -60,11 +62,8 @@ type SelectImageBuildExportCardProps = {

export const SelectImageBuildExportCard = ({ format, isChecked, onToggle }: SelectImageBuildExportCardProps) => {
const { t } = useTranslation();

const title = getExportFormatLabel(t, format);
const description = getExportFormatDescription(t, format);

const id = `export-format-${format}`;

return (
<Card id={id} isSelectable isSelected={isChecked} className="fctl-imageexport-card">
<CardHeader
Expand All @@ -81,12 +80,12 @@ export const SelectImageBuildExportCard = ({ format, isChecked, onToggle }: Sele
</FlexItem>
<FlexItem>
<Content>
<Content component={ContentVariants.h2}>{title}</Content>
<Content component={ContentVariants.h2}>{getExportFormatLabel(t, format)}</Content>
</Content>
</FlexItem>
</Flex>
</CardHeader>
<CardBody>{description}</CardBody>
<CardBody>{getExportFormatDescription(t, format)}</CardBody>
</Card>
);
};
Expand All @@ -110,15 +109,9 @@ export const ViewImageBuildExportCard = ({
} = useAppContext();
const routerNavigate = useRouterNavigate();
const exists = !!imageExport;

const exportReason = exists ? getImageExportStatusReason(imageExport) : undefined;
const title = getExportFormatLabel(t, format);
const description = getExportFormatDescription(t, format);

const handleViewLogs = () => {
const baseRoute = appRoutes[ROUTE.IMAGE_BUILD_DETAILS];
routerNavigate(`${baseRoute}/${imageBuildId}/logs`);
};
const { checkPermissions } = usePermissionsContext();
const [canViewLogs] = checkPermissions([{ kind: RESOURCE.IMAGE_EXPORT_LOG, verb: VERB.GET }]);

return (
<Card isLarge className="fctl-imageexport-card">
Expand Down Expand Up @@ -148,17 +141,17 @@ export const ViewImageBuildExportCard = ({
</FlexItem>
<FlexItem>
<Content>
<Content component={ContentVariants.h2}>{title}</Content>
<Content component={ContentVariants.h2}>{getExportFormatLabel(t, format)}</Content>
</Content>
</FlexItem>
</Flex>
</CardHeader>
<CardBody>{description}</CardBody>
<CardBody>{getExportFormatDescription(t, format)}</CardBody>
<CardFooter>
<Stack hasGutter>
<StackItem>
<Flex>
{exportReason === ImageExportConditionReason.ImageExportConditionReasonFailed && (
{exportReason === ImageExportConditionReason.ImageExportConditionReasonFailed && onExportImage && (
<FlexItem>
<Button
variant="primary"
Expand All @@ -170,7 +163,7 @@ export const ViewImageBuildExportCard = ({
</Button>
</FlexItem>
)}
{exportReason === ImageExportConditionReason.ImageExportConditionReasonCompleted && (
{exportReason === ImageExportConditionReason.ImageExportConditionReasonCompleted && onDownload && (
<FlexItem>
<Button
variant="secondary"
Expand All @@ -182,12 +175,21 @@ export const ViewImageBuildExportCard = ({
</Button>
</FlexItem>
)}
<FlexItem>
{exists ? (
<Button variant="secondary" onClick={handleViewLogs}>
{exists && canViewLogs && (
<FlexItem>
<Button
variant="secondary"
onClick={() => {
const baseRoute = appRoutes[ROUTE.IMAGE_BUILD_DETAILS];
routerNavigate(`${baseRoute}/${imageBuildId}/logs`);
}}
>
{t('View logs')}
</Button>
) : (
</FlexItem>
)}
{!exists && onExportImage && (
<FlexItem>
<Button
variant="secondary"
onClick={() => onExportImage(format)}
Expand All @@ -196,8 +198,8 @@ export const ViewImageBuildExportCard = ({
>
{t('Export image')}
</Button>
)}
</FlexItem>
</FlexItem>
)}
</Flex>
</StackItem>
<StackItem>
Expand Down
4 changes: 4 additions & 0 deletions libs/ui-components/src/types/rbac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ export enum RESOURCE {
ALERTS = 'alerts',
AUTH_PROVIDER = 'authproviders',
IMAGE_BUILD = 'imagebuilds',
IMAGE_BUILD_LOG = 'imagebuilds/log',
IMAGE_EXPORT = 'imageexports',
IMAGE_EXPORT_LOG = 'imageexports/log',
IMAGE_EXPORT_DOWNLOAD = 'imageexports/download',
}