Skip to content

Commit f44b5b2

Browse files
committed
blueprints: enable exporting blueprints in cockpit
Export the blueprint as TOML on premise, and not as JSON. The expectation is that users might want to use this blueprint with composer-cli or image-builder-cli. Because the behaviour is different between the cockpit and hosted UIs, refrain from making the backend api abstraction.
1 parent 1ba4ee6 commit f44b5b2

File tree

2 files changed

+63
-8
lines changed

2 files changed

+63
-8
lines changed

src/Components/Blueprints/BlueprintActionsMenu.tsx

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@ import {
88
} from '@patternfly/react-core';
99
import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle/MenuToggle';
1010
import { EllipsisVIcon } from '@patternfly/react-icons';
11+
import TOML from 'smol-toml';
1112

13+
// The hosted UI exports JSON, while the Cockpit plugin exports TOML.
14+
// Because the blueprint formats differ, using the 'backendApi'
15+
// abstraction would be misleading. Import and handle each environment
16+
// separately.
1217
import { selectSelectedBlueprintId } from '../../store/BlueprintSlice';
18+
import { useLazyExportBlueprintCockpitQuery } from '../../store/cockpit/cockpitApi';
19+
import type { Blueprint as CockpitExportResponse } from '../../store/cockpit/composerCloudApi';
1320
import { useAppSelector } from '../../store/hooks';
1421
import {
1522
BlueprintExportResponse,
@@ -30,6 +37,7 @@ export const BlueprintActionsMenu: React.FunctionComponent<
3037
};
3138

3239
const [trigger] = useLazyExportBlueprintQuery();
40+
const [cockpitTrigger] = useLazyExportBlueprintCockpitQuery();
3341
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
3442
if (selectedBlueprintId === undefined) {
3543
return null;
@@ -41,6 +49,15 @@ export const BlueprintActionsMenu: React.FunctionComponent<
4149
handleExportBlueprint(response.name, response);
4250
});
4351
};
52+
53+
const handleCockpitClick = () => {
54+
cockpitTrigger({ id: selectedBlueprintId })
55+
.unwrap()
56+
.then((response: CockpitExportResponse) => {
57+
handleExportBlueprint(response.name, response);
58+
});
59+
};
60+
4461
return (
4562
<Dropdown
4663
isOpen={showBlueprintActionsMenu}
@@ -62,8 +79,10 @@ export const BlueprintActionsMenu: React.FunctionComponent<
6279
)}
6380
>
6481
<DropdownList>
65-
<DropdownItem onClick={handleClick}>
66-
Download blueprint (.json)
82+
<DropdownItem
83+
onClick={process.env.IS_ON_PREMISE ? handleCockpitClick : handleClick}
84+
>
85+
Download blueprint ({process.env.IS_ON_PREMISE ? '.toml' : '.json'})
6786
</DropdownItem>
6887
<DropdownItem onClick={() => setShowDeleteModal(true)}>
6988
Delete blueprint
@@ -75,15 +94,28 @@ export const BlueprintActionsMenu: React.FunctionComponent<
7594

7695
async function handleExportBlueprint(
7796
blueprintName: string,
78-
blueprint: BlueprintExportResponse,
97+
blueprint: BlueprintExportResponse | CockpitExportResponse,
7998
) {
80-
const jsonData = JSON.stringify(blueprint, null, 2);
81-
const blob = new Blob([jsonData], { type: 'application/json' });
99+
const data = process.env.IS_ON_PREMISE
100+
? TOML.stringify(blueprint)
101+
: JSON.stringify(blueprint, null, 2);
102+
const mime = process.env.IS_ON_PREMISE
103+
? 'application/octet-stream'
104+
: 'application/json';
105+
const blob = new Blob([data], { type: mime });
82106

83107
const url = URL.createObjectURL(blob);
84-
const link = document.createElement('a');
108+
// In cockpit we're running in an iframe, the current content-security policy
109+
// (set in cockpit/public/manifest.json) only allows resources from the same origin as the
110+
// document (which is unique to the iframe). So create the element in the parent document.
111+
const link = process.env.IS_ON_PREMISE
112+
? window.parent.document.createElement('a')
113+
: document.createElement('a');
85114
link.href = url;
86-
link.download = blueprintName.replace(/\s/g, '_').toLowerCase() + '.json';
115+
link.download =
116+
blueprintName.replace(/\s/g, '_').toLowerCase() + process.env.IS_ON_PREMISE
117+
? '.toml'
118+
: '.json';
87119
link.click();
88120
URL.revokeObjectURL(url);
89121
}

src/store/cockpit/cockpitApi.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import { fsinfo } from 'cockpit/fsinfo';
1111
import TOML from 'smol-toml';
1212
import { v4 as uuidv4 } from 'uuid';
1313

14-
import { Customizations } from './composerCloudApi';
14+
import type {
15+
Blueprint as CloudApiBlueprint,
16+
Customizations,
17+
} from './composerCloudApi';
1518
// We have to work around RTK query here, since it doesn't like splitting
1619
// out the same api into two separate apis. So, instead, we can just
1720
// inherit/import the `contentSourcesApi` and build on top of that.
@@ -45,6 +48,7 @@ import {
4548
DeleteBlueprintApiArg,
4649
DeleteBlueprintApiResponse,
4750
DistributionProfileItem,
51+
ExportBlueprintApiArg,
4852
GetArchitecturesApiArg,
4953
GetArchitecturesApiResponse,
5054
GetBlueprintApiArg,
@@ -407,6 +411,23 @@ export const cockpitApi = contentSourcesApi.injectEndpoints({
407411
}
408412
},
409413
}),
414+
exportBlueprintCockpit: builder.query<
415+
CloudApiBlueprint,
416+
ExportBlueprintApiArg
417+
>({
418+
queryFn: async ({ id }) => {
419+
const blueprintsDir = await getBlueprintsPath();
420+
const file = cockpit.file(path.join(blueprintsDir, id, `${id}.json`));
421+
const contents = await file.read();
422+
const blueprint = JSON.parse(
423+
contents,
424+
) as CockpitCreateBlueprintRequest;
425+
const onPrem = mapHostedToOnPrem(blueprint as CreateBlueprintRequest);
426+
return {
427+
data: onPrem,
428+
};
429+
},
430+
}),
410431
getOscapProfiles: builder.query<
411432
GetOscapProfilesApiResponse,
412433
GetOscapProfilesApiArg
@@ -762,6 +783,8 @@ export const {
762783
useCreateBlueprintMutation,
763784
useUpdateBlueprintMutation,
764785
useDeleteBlueprintMutation,
786+
useExportBlueprintCockpitQuery,
787+
useLazyExportBlueprintCockpitQuery,
765788
useGetOscapProfilesQuery,
766789
useGetOscapCustomizationsQuery,
767790
useLazyGetOscapCustomizationsQuery,

0 commit comments

Comments
 (0)