-
Notifications
You must be signed in to change notification settings - Fork 335
Implement Application Configuration Export & Import (CasC Support) #9566
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
WalkthroughImplements application configuration export and import (CasC) by adding API methods for export/import, a new export modal, list and page UI integrations for export/import flows, and corresponding i18n keys and a changeset. Changes
sequenceDiagram
autonumber
participant User as User
participant FE as Frontend (UI)
participant API as API Client
participant BE as Backend (Server)
rect rgba(200,220,255,0.5)
User->>FE: Click "Export" on application
FE->>FE: Open ExportApplicationModal (choose format & secrets)
FE->>API: POST /applications/{id}/export?format=&exportSecrets=
API->>BE: HTTP request (export)
BE-->>API: 200 OK + file blob
API-->>FE: Blob response
FE->>User: Trigger file download
end
rect rgba(200,255,200,0.5)
User->>FE: Click "Import" and select file
FE->>API: POST /applications/import (multipart/form-data)
API->>BE: HTTP request (upload file)
BE-->>API: 200/201 OK (import result)
API-->>FE: Response (success/error)
FE->>FE: Refresh application list / show alert
FE->>User: Show success or error message
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@features/admin.applications.v1/api/application.ts`:
- Around line 1389-1408: The getImportContentType function is dead code (defined
as getImportContentType) and never referenced by importApplication; either
delete getImportContentType to remove unused code, or update importApplication
to call getImportContentType(fileName) and use its result for the request
Content-Type header instead of the hardcoded "multipart/form-data" value so the
correct MIME is sent based on the file extension.
In `@features/admin.applications.v1/components/application-list.tsx`:
- Around line 353-372: The catch block in application-list.tsx assumes
error.response.data is JSON, but when exportApplication uses responseType:
"blob" the server error comes back as a Blob; update the error handling in that
catch to detect if error.response.data is a Blob (instanceof Blob) and, if so,
read it (e.g. via .text() or FileReader) and JSON.parse the text to extract
error.response.data.description before dispatching addAlert; otherwise keep the
existing checks for error.response.data.description. You can implement this
parsing inline in the catch or extract it to a helper like parseBlobError to
return the parsed object and then dispatch the alert with the parsed description
when available.
🧹 Nitpick comments (2)
features/admin.applications.v1/pages/applications.tsx (1)
647-664: Consider addingaria-labelfor the hidden file input for accessibility.While the file input is hidden and triggered programmatically, adding an
aria-labelhelps screen readers understand the control's purpose.Suggested improvement
<input ref={ fileInputRef } type="file" accept=".xml,.json,.yaml,.yml" hidden onChange={ handleApplicationImport } data-testid={ `${ testId }-import-file-input` } + aria-label={ t("applications:list.actions.import") } />features/admin.applications.v1/components/modals/export-application-modal.tsx (1)
64-96: Consider resetting state when modal is opened fresh.The component's
exportSecretsandexportFormatstate persists between modal opens. If a user opens the modal, changes options, closes without exporting, then opens for a different application, the previous selections remain. Consider resetting state when the modal opens or whenapplicationNamechanges.Suggested improvement using useEffect
+import React, { ChangeEvent, FunctionComponent, ReactElement, useState, useEffect } from "react"; ... const ExportApplicationModal: FunctionComponent<ExportApplicationModalProps> = ({ "data-componentid": componentId = "export-application-modal", applicationName, onExport, onClose, open, ...rest }: ExportApplicationModalProps): ReactElement => { const { t } = useTranslation(); const [ exportSecrets, setExportSecrets ] = useState<boolean>(false); const [ exportFormat, setExportFormat ] = useState<ExportFormat>("xml"); + + // Reset state when modal opens + useEffect(() => { + if (open) { + setExportSecrets(false); + setExportFormat("xml"); + } + }, [ open ]);
| /** | ||
| * Gets the content type based on file extension. | ||
| * | ||
| * @param fileName - The name of the file. | ||
| * @returns The appropriate content type for the file. | ||
| */ | ||
| const getImportContentType = (fileName: string): string => { | ||
| const extension: string = fileName.split(".").pop()?.toLowerCase() || ""; | ||
|
|
||
| switch (extension) { | ||
| case "json": | ||
| return "application/json"; | ||
| case "yaml": | ||
| case "yml": | ||
| return "application/yaml"; | ||
| case "xml": | ||
| default: | ||
| return "application/xml"; | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dead code: getImportContentType is defined but never used.
The getImportContentType function determines the content type based on file extension, but importApplication uses hardcoded "multipart/form-data" as the Content-Type. Either use this function or remove it.
Suggested fix: Remove unused function
-/**
- * Gets the content type based on file extension.
- *
- * `@param` fileName - The name of the file.
- * `@returns` The appropriate content type for the file.
- */
-const getImportContentType = (fileName: string): string => {
- const extension: string = fileName.split(".").pop()?.toLowerCase() || "";
-
- switch (extension) {
- case "json":
- return "application/json";
- case "yaml":
- case "yml":
- return "application/yaml";
- case "xml":
- default:
- return "application/xml";
- }
-};
-📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Gets the content type based on file extension. | |
| * | |
| * @param fileName - The name of the file. | |
| * @returns The appropriate content type for the file. | |
| */ | |
| const getImportContentType = (fileName: string): string => { | |
| const extension: string = fileName.split(".").pop()?.toLowerCase() || ""; | |
| switch (extension) { | |
| case "json": | |
| return "application/json"; | |
| case "yaml": | |
| case "yml": | |
| return "application/yaml"; | |
| case "xml": | |
| default: | |
| return "application/xml"; | |
| } | |
| }; |
🤖 Prompt for AI Agents
In `@features/admin.applications.v1/api/application.ts` around lines 1389 - 1408,
The getImportContentType function is dead code (defined as getImportContentType)
and never referenced by importApplication; either delete getImportContentType to
remove unused code, or update importApplication to call
getImportContentType(fileName) and use its result for the request Content-Type
header instead of the hardcoded "multipart/form-data" value so the correct MIME
is sent based on the file extension.
| .catch((error: AxiosError) => { | ||
| if (error.response && error.response.data && error.response.data.description) { | ||
| dispatch(addAlert({ | ||
| description: error.response.data.description, | ||
| level: AlertLevels.ERROR, | ||
| message: t("applications:notifications.exportApplication.error" + | ||
| ".message") | ||
| })); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| dispatch(addAlert({ | ||
| description: t("applications:notifications.exportApplication" + | ||
| ".genericError.description"), | ||
| level: AlertLevels.ERROR, | ||
| message: t("applications:notifications.exportApplication.genericError" + | ||
| ".message") | ||
| })); | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blob response error handling may not extract error description correctly.
When exportApplication uses responseType: "blob", error responses from the server will also be received as a Blob. Accessing error.response.data.description on a Blob object will return undefined, causing the generic error message to always be shown instead of the server's specific error message.
Suggested fix: Parse blob error response
.catch((error: AxiosError) => {
- if (error.response && error.response.data && error.response.data.description) {
- dispatch(addAlert({
- description: error.response.data.description,
- level: AlertLevels.ERROR,
- message: t("applications:notifications.exportApplication.error" +
- ".message")
- }));
-
- return;
- }
-
- dispatch(addAlert({
- description: t("applications:notifications.exportApplication" +
- ".genericError.description"),
- level: AlertLevels.ERROR,
- message: t("applications:notifications.exportApplication.genericError" +
- ".message")
- }));
+ if (error.response?.data instanceof Blob) {
+ error.response.data.text().then((text: string) => {
+ try {
+ const errorData = JSON.parse(text);
+ dispatch(addAlert({
+ description: errorData.description || t("applications:notifications.exportApplication.genericError.description"),
+ level: AlertLevels.ERROR,
+ message: t("applications:notifications.exportApplication.error.message")
+ }));
+ } catch {
+ dispatch(addAlert({
+ description: t("applications:notifications.exportApplication.genericError.description"),
+ level: AlertLevels.ERROR,
+ message: t("applications:notifications.exportApplication.genericError.message")
+ }));
+ }
+ });
+ } else {
+ dispatch(addAlert({
+ description: t("applications:notifications.exportApplication.genericError.description"),
+ level: AlertLevels.ERROR,
+ message: t("applications:notifications.exportApplication.genericError.message")
+ }));
+ }
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .catch((error: AxiosError) => { | |
| if (error.response && error.response.data && error.response.data.description) { | |
| dispatch(addAlert({ | |
| description: error.response.data.description, | |
| level: AlertLevels.ERROR, | |
| message: t("applications:notifications.exportApplication.error" + | |
| ".message") | |
| })); | |
| return; | |
| } | |
| dispatch(addAlert({ | |
| description: t("applications:notifications.exportApplication" + | |
| ".genericError.description"), | |
| level: AlertLevels.ERROR, | |
| message: t("applications:notifications.exportApplication.genericError" + | |
| ".message") | |
| })); | |
| }) | |
| .catch((error: AxiosError) => { | |
| if (error.response?.data instanceof Blob) { | |
| error.response.data.text().then((text: string) => { | |
| try { | |
| const errorData = JSON.parse(text); | |
| dispatch(addAlert({ | |
| description: errorData.description || t("applications:notifications.exportApplication.genericError.description"), | |
| level: AlertLevels.ERROR, | |
| message: t("applications:notifications.exportApplication.error.message") | |
| })); | |
| } catch { | |
| dispatch(addAlert({ | |
| description: t("applications:notifications.exportApplication.genericError.description"), | |
| level: AlertLevels.ERROR, | |
| message: t("applications:notifications.exportApplication.genericError.message") | |
| })); | |
| } | |
| }); | |
| } else { | |
| dispatch(addAlert({ | |
| description: t("applications:notifications.exportApplication.genericError.description"), | |
| level: AlertLevels.ERROR, | |
| message: t("applications:notifications.exportApplication.genericError.message") | |
| })); | |
| } | |
| }) |
🤖 Prompt for AI Agents
In `@features/admin.applications.v1/components/application-list.tsx` around lines
353 - 372, The catch block in application-list.tsx assumes error.response.data
is JSON, but when exportApplication uses responseType: "blob" the server error
comes back as a Blob; update the error handling in that catch to detect if
error.response.data is a Blob (instanceof Blob) and, if so, read it (e.g. via
.text() or FileReader) and JSON.parse the text to extract
error.response.data.description before dispatching addAlert; otherwise keep the
existing checks for error.response.data.description. You can implement this
parsing inline in the catch or extract it to a helper like parseBlobError to
return the parsed object and then dispatch the alert with the parsed description
when available.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #9566 +/- ##
=======================================
Coverage 55.88% 55.88%
=======================================
Files 42 42
Lines 1020 1020
Branches 254 231 -23
=======================================
Hits 570 570
Misses 450 450
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
Related Issue
Implementation
This PR introduces the ability for administrators to Export and Import application configurations directly from the Console UI. This is a key step towards supporting Configuration as Code (CasC), allowing users to migrate application settings between different environments or back up configurations easily.
Key Changes
Added exportApplication and importApplication functions in features/admin.applications.v1/api/application.ts.
Support for multiple file formats: XML, JSON, and YAML.
Implemented dynamic Accept and Content-Type header handling based on the selected file format.
Added support for the exportSecrets flag to include/exclude sensitive credentials during export.
Export Application Modal: A new modal component (export-application-modal.tsx) that allows users to:
Choose whether to include secrets (with a security warning).
Select the desired file format (XML, JSON, YAML).
Application List Enhancements: Added an "Export" action icon to the application list table.
Import Functionality: Added an "Import" button to the main Applications page with a file picker that accepts .xml, .json, .yaml, .yml files.
Added comprehensive translation strings for both Export and Import workflows, including success/error notifications and helpful hints regarding security risks when exporting secrets.
Included a Changeset to track minor version updates for the affected packages.
Demonstration
Screen.Recording.2026-01-23.at.8.57.19.AM.mov
Summary by CodeRabbit
New Features
Documentation / Localization
✏️ Tip: You can customize this high-level summary in your review settings.