Skip to content
Merged
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
36 changes: 28 additions & 8 deletions libs/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,24 @@
"Set the maximum memory usage for your container using Podman format. You can specify a number with an optional unit: \"b\" (bytes), \"k\" (kibibytes), \"m\" (mebibytes), \"g\" (gibibytes). Examples: \"512\", \"512m\", \"1g\", \"2048k\". Ensure the limit fits within your device's available memory and accounts for other applications and system processes.": "Set the maximum memory usage for your container using Podman format. You can specify a number with an optional unit: \"b\" (bytes), \"k\" (kibibytes), \"m\" (mebibytes), \"g\" (gibibytes). Examples: \"512\", \"512m\", \"1g\", \"2048k\". Ensure the limit fits within your device's available memory and accounts for other applications and system processes.",
"Enter numeric value with optional unit": "Enter numeric value with optional unit",
"Provide a valid memory value (e.g., \"512\", \"512m\", \"2g\", \"1024k\").": "Provide a valid memory value (e.g., \"512\", \"512m\", \"2g\", \"1024k\").",
"OCI reference URL": "OCI reference URL",
"Reference to the OCI image or artifact containing the Helm chart.": "Reference to the OCI image or artifact containing the Helm chart.",
"Namespace": "Namespace",
"The namespace to install the Helm chart into.": "The namespace to install the Helm chart into.",
"Type namespace here": "Type namespace here",
"If you do not specify a namespace, the agent uses a namespace based on the application name.": "If you do not specify a namespace, the agent uses a namespace based on the application name.",
"Values file names": "Values file names",
"Reference values files that are defined within the Helm application package. Files are applied in the specified order, before user-provided values.": "Reference values files that are defined within the Helm application package. Files are applied in the specified order, before user-provided values.",
"Values file {{ number }}": "Values file {{ number }}",
"Enter a file path relative to the Helm chart root. For example, values.yaml": "Enter a file path relative to the Helm chart root. For example, values.yaml",
"Type values file name here": "Type values file name here",
"Delete values file": "Delete values file",
"Fill in the existing values files before you can add more values files.": "Fill in the existing values files before you can add more values files.",
"Add values file": "Add values file",
"Order of precedence: Files are applied in the ordered listed. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.": "Order of precedence: Files are applied in the ordered listed. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.",

@coderabbitai coderabbitai Bot Jan 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix typo in user-facing string.

"ordered listed" should be "order listed" or "listed order".

✏️ Proposed fix
-"Order of precedence: Files are applied in the ordered listed. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.": "Order of precedence: Files are applied in the ordered listed. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.",
+"Order of precedence: Files are applied in the listed order. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.": "Order of precedence: Files are applied in the listed order. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.",
📝 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.

Suggested change
"Order of precedence: Files are applied in the ordered listed. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.": "Order of precedence: Files are applied in the ordered listed. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.",
"Order of precedence: Files are applied in the listed order. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.": "Order of precedence: Files are applied in the listed order. If the same parameter is defined in multiple files, the value in the last file takes precedence and overrides previous values.",
🤖 Prompt for AI Agents
In `@libs/i18n/locales/en/translation.json` at line 391, Fix the typo in the
translation entry whose key/value is "Order of precedence: Files are applied in
the ordered listed. If the same parameter is defined in multiple files, the
value in the last file takes precedence and overrides previous values." — change
"ordered listed" to "listed order" (or "order listed") consistently in both the
JSON key and its value so the user-facing string reads correctly (e.g., "Order
of precedence: Files are applied in the listed order. ...").

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix in a separate PR

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@celdrake, sounds good! Thanks for catching that—I'll look for the fix in the follow-up PR.


🧠 Learnings used
Learnt from: celdrake
Repo: flightctl/flightctl-ui PR: 207
File: libs/ui-components/src/components/Device/EditDeviceWizard/steps/ReviewDeviceStep.tsx:55-55
Timestamp: 2025-02-04T09:04:36.106Z
Learning: The codebase has automated CI checks ("lint" step) that verify translation coverage for all text strings, ensuring that new or modified strings have corresponding entries in translation files.

Learnt from: celdrake
Repo: flightctl/flightctl-ui PR: 371
File: libs/types/models/AppType.ts:10-10
Timestamp: 2025-10-29T16:47:29.614Z
Learning: PR `#371` (flightctl/flightctl-ui) adds the AppTypeQuadlet enum member and related types as a preparatory change. Full implementation of quadlet application support in the UI will be added later, after backend support is available.

"Inline values": "Inline values",
"Provide a valid YAML file": "Provide a valid YAML file",
"Enter configuration values in YAML format to be applied to the Helm chart. These values take precedence over those defined in the files listed above.": "Enter configuration values in YAML format to be applied to the Helm chart. These values take precedence over those defined in the files listed above.",
"The application image. Learn how to create one": "The application image. Learn how to create one",
"here": "here",
"File {{ fileNum }}": "File {{ fileNum }}",
Expand All @@ -382,9 +400,6 @@
"Content is base64 encoded": "Content is base64 encoded",
"Delete file": "Delete file",
"Add file": "Add file",
"Single Container application": "Single Container application",
"Quadlet application": "Quadlet application",
"Compose application": "Compose application",
"Application {{ appNum }}": "Application {{ appNum }}",
"Application type": "Application type",
"Select an application type": "Select an application type",
Expand All @@ -394,7 +409,6 @@
"Pull definitions from container registry (reusable, versioned).": "Pull definitions from container registry (reusable, versioned).",
"Inline": "Inline",
"Define application files directly in this interface (custom, one-off).": "Define application files directly in this interface (custom, one-off).",
"OCI reference URL": "OCI reference URL",
"The unique identifier for this application.": "The unique identifier for this application.",
"Variable {{ number }}": "Variable {{ number }}",
"Delete variable": "Delete variable",
Expand Down Expand Up @@ -484,10 +498,6 @@
"The device will download and apply updates as soon as they are available.": "The device will download and apply updates as soon as they are available.",
"Device alias": "Device alias",
"Device labels": "Device labels",
"Single Container": "Single Container",
"Quadlet": "Quadlet",
"Compose": "Compose",
"Unknown": "Unknown",
"Unnamed": "Unnamed",
"Device fleet": "Device fleet",
"Edge Manager will not manage system image": "Edge Manager will not manage system image",
Expand Down Expand Up @@ -699,6 +709,7 @@
"Add item": "Add item",
"Resolved": "Resolved",
"Name must be unique": "Name must be unique",
"Unknown": "Unknown",
"Accessible": "Accessible",
"Not accessible": "Not accessible",
"Missing repository": "Missing repository",
Expand Down Expand Up @@ -755,6 +766,7 @@
"Container port is required": "Container port is required",
"CPU limit is invalid.": "CPU limit is invalid.",
"Memory limit is invalid.": "Memory limit is invalid.",
"YAML content is invalid.": "YAML content is invalid.",
"Name is required for quadlet applications.": "Name is required for quadlet applications.",
"Name is required for compose applications.": "Name is required for compose applications.",
"Application name must be unique.": "Application name must be unique.",
Expand Down Expand Up @@ -1161,6 +1173,14 @@
"Product UUID": "Product UUID",
"TPM vendor info": "TPM vendor info",
"Websocket error occured": "Websocket error occured",
"Single Container application": "Single Container application",
"Quadlet application": "Quadlet application",
"Helm application": "Helm application",
"Compose application": "Compose application",
"Single Container": "Single Container",
"Quadlet": "Quadlet",
"Compose": "Compose",
"Helm": "Helm",
"OpenShift": "OpenShift",
"Kubernetes": "Kubernetes",
"Ansible Automation Platform": "Ansible Automation Platform",
Expand Down
1 change: 1 addition & 0 deletions libs/types/models/AppType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export enum AppType {
AppTypeCompose = 'compose',
AppTypeQuadlet = 'quadlet',
AppTypeContainer = 'container',
AppTypeHelm = 'helm',
}
12 changes: 12 additions & 0 deletions libs/types/models/ImageApplicationProviderSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ export type ImageApplicationProviderSpec = (ApplicationVolumeProviderSpec & {
* Reference to the OCI image or artifact for the application package.
*/
image: string;
/**
* Kubernetes namespace for helm chart installation. Only applicable when appType is 'helm'.
*/
namespace?: string;
/**
* Helm values to pass during install/upgrade. Supports arbitrarily nested YAML structures. Only applicable when appType is 'helm'.
*/
values?: Record<string, any>;
/**
* List of values files from within the chart to use during install/upgrade. Files are relative to chart root and are applied in array order before user-provided values. Only applicable when appType is 'helm'.
*/
valuesFiles?: Array<string>;
/**
* Port mappings.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import { DeviceApplicationStatus } from '@flightctl/types';
import { useTranslation } from '../../../hooks/useTranslation';
import ApplicationStatus from '../../Status/ApplicationStatus';
import { getAppTypeLabel } from '../../../utils/apps';

type ApplicationsTableProps = {
appsStatus: DeviceApplicationStatus[];
Expand Down Expand Up @@ -39,7 +40,9 @@ const ApplicationsTable = ({ appsStatus }: ApplicationsTableProps) => {
</Td>
<Td dataLabel={t('Ready')}>{app.ready}</Td>
<Td dataLabel={t('Restarts')}>{app.restarts}</Td>
<Td dataLabel={t('Type')}>{app.appType ? <Label variant="outline">{app.appType}</Label> : '-'}</Td>
<Td dataLabel={t('Type')}>
{app.appType ? <Label variant="outline">{getAppTypeLabel(app.appType, t)}</Label> : '-'}
</Td>
<Td dataLabel={t('Embedded')}>{app.embedded ? t('Yes') : t('No')}</Td>
</Tr>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import yaml from 'js-yaml';
import {
AppType,
// eslint-disable-next-line no-restricted-imports
Expand Down Expand Up @@ -38,6 +39,7 @@ import {
isComposeImageAppForm,
isGitConfigTemplate,
isGitProviderSpec,
isHelmImageAppForm,
isHttpConfigTemplate,
isHttpProviderSpec,
isImageAppProvider,
Expand Down Expand Up @@ -215,6 +217,32 @@ export const getDeviceSpecConfigPatches = (
};

export const toAPIApplication = (app: AppForm): ApplicationProviderSpec => {
if (isHelmImageAppForm(app)) {
const data: ImageApplicationProviderSpec & ApplicationProviderSpec = {
name: app.name,
image: app.image,
appType: app.appType,
};
if (app.namespace) {
data.namespace = app.namespace;
}
if (app.valuesYaml) {
try {
const values = yaml.load(app.valuesYaml) as Record<string, unknown>;
if (values && Object.keys(values).length > 0) {
data.values = values;
}
} catch (error) {
throw new Error('Values content is not valid YAML.');
}
}
const fileNames = app.valuesFiles.filter((file) => file && file.trim() !== '');
if (fileNames.length > 0) {
data.valuesFiles = fileNames;
}
return data;
}
Comment on lines +220 to +244

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "deviceSpecUtils.ts" -type f | head -20

Repository: flightctl/flightctl-ui

Length of output: 144


🏁 Script executed:

find . -name "EditDeviceWizard*" -type f | head -20

Repository: flightctl/flightctl-ui

Length of output: 383


🏁 Script executed:

rg -l "valuesYaml" --type ts --type tsx | head -20

Repository: flightctl/flightctl-ui

Length of output: 93


🏁 Script executed:

head -n 250 ./libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts | tail -n 40

Repository: flightctl/flightctl-ui

Length of output: 1094


🏁 Script executed:

rg "app\.valuesYaml" --type ts | head -20

Repository: flightctl/flightctl-ui

Length of output: 322


🏁 Script executed:

rg "valuesYaml" -A 3 -B 3 ./libs/ui-components/src/components/Device/EditDeviceWizard/EditDeviceWizard.tsx | head -60

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

rg "type AppForm\|interface AppForm" --type ts -A 10 | head -50

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

rg "YamlEditor" --type ts -B 3 -A 3 | head -80

Repository: flightctl/flightctl-ui

Length of output: 8291


🏁 Script executed:

grep -r "valuesYaml" ./libs/ui-components/src/components/Device/EditDeviceWizard/ | head -20

Repository: flightctl/flightctl-ui

Length of output: 1007


🏁 Script executed:

cat ./libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationHelmForm.tsx

Repository: flightctl/flightctl-ui

Length of output: 6260


🏁 Script executed:

rg "UploadField" -A 5 -B 5 ./libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationHelmForm.tsx

Repository: flightctl/flightctl-ui

Length of output: 1025


🏁 Script executed:

rg "type AppForm|interface AppForm" -r . --type ts --type tsx | head -20

Repository: flightctl/flightctl-ui

Length of output: 93


🏁 Script executed:

find . -name "deviceSpec.ts" -o -name "deviceSpec.tsx" | head -10

Repository: flightctl/flightctl-ui

Length of output: 110


🏁 Script executed:

rg "export.*HelmImageAppForm|interface HelmImageAppForm|type HelmImageAppForm" --type ts -A 8

Repository: flightctl/flightctl-ui

Length of output: 1375


🏁 Script executed:

find . -name "UploadField.tsx" -o -name "UploadField.ts" | head -5

Repository: flightctl/flightctl-ui

Length of output: 122


🏁 Script executed:

cat ./libs/ui-components/src/components/form/UploadField.tsx

Repository: flightctl/flightctl-ui

Length of output: 4511


🏁 Script executed:

rg "toAPIApplication" -B 5 -A 2 ./libs/ui-components/src/components/Device/EditDeviceWizard/EditDeviceWizard.tsx

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

rg "toAPIApplication" --type ts | head -20

Repository: flightctl/flightctl-ui

Length of output: 802


🏁 Script executed:

rg "toAPIApplication\(" -B 10 -A 2 | head -60

Repository: flightctl/flightctl-ui

Length of output: 1548


🏁 Script executed:

cat -n ./libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts | sed -n '210,250p'

Repository: flightctl/flightctl-ui

Length of output: 1404


🏁 Script executed:

rg "yaml\.load" --type ts -B 2 -A 2 | head -40

Repository: flightctl/flightctl-ui

Length of output: 1668


🏁 Script executed:

rg "valuesYaml" ./libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts -B 2 -A 10 | head -80

Repository: flightctl/flightctl-ui

Length of output: 1349


🏁 Script executed:

rg "valuesYaml" ./libs/ui-components/src/components/form/validations.ts -B 10 -A 10

Repository: flightctl/flightctl-ui

Length of output: 1047


🏁 Script executed:

rg "yaml.load.*Record" --type ts -B 3 -A 3

Repository: flightctl/flightctl-ui

Length of output: 1683


🏁 Script executed:

cat ./libs/ui-components/src/components/form/validations.ts | head -100

Repository: flightctl/flightctl-ui

Length of output: 3770


🏁 Script executed:

rg "yaml\.load.*as Record" --type ts -B 5 -A 5

Repository: flightctl/flightctl-ui

Length of output: 2495


🏁 Script executed:

cat ./libs/ui-components/src/components/form/validations.ts | sed -n '60,85p'

Repository: flightctl/flightctl-ui

Length of output: 1201


🏁 Script executed:

cat ./libs/ui-components/src/components/form/validations.ts | sed -n '160,190p'

Repository: flightctl/flightctl-ui

Length of output: 1050


🏁 Script executed:

rg "valuesYaml.*Yup" --type ts -A 12

Repository: flightctl/flightctl-ui

Length of output: 1306


🏁 Script executed:

cat ./libs/ui-components/src/components/form/validations.ts | sed -n '370,405p'

Repository: flightctl/flightctl-ui

Length of output: 1100


🏁 Script executed:

cat -n ./libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts | sed -n '250,270p'

Repository: flightctl/flightctl-ui

Length of output: 711


🏁 Script executed:

python3 << 'EOF'
import json

# Simulate yaml.load behavior with different inputs
test_cases = [
    ("hello", "scalar string"),
    ("123", "scalar number"),
    ("[1, 2, 3]", "array"),
    ("key: value", "object/mapping"),
    ("null", "null value"),
]

for yaml_str, description in test_cases:
    print(f"\n{description}: {yaml_str}")
    print(f"  typeof check: typeof would be 'object' for arrays/objects/null, 'string'/'number' for scalars")
    if yaml_str in ["123", "hello"]:
        print(f"  Object.keys() would fail or return empty")
    elif yaml_str == "null":
        print(f"  Object.keys(null) would throw TypeError")
    elif yaml_str.startswith("["):
        print(f"  Object.keys() would return numeric indices: ['0', '1', '2']")
    else:
        print(f"  Object.keys() would work correctly for object")
EOF

Repository: flightctl/flightctl-ui

Length of output: 152


🏁 Script executed:

cat ./libs/ui-components/src/components/form/validations.ts | sed -n '395,425p'

Repository: flightctl/flightctl-ui

Length of output: 1037


🏁 Script executed:

cat ./libs/ui-components/src/components/form/validations.ts | sed -n '327,345p'

Repository: flightctl/flightctl-ui

Length of output: 874


🏁 Script executed:

cat -n ./libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts | sed -n '255,275p'

Repository: flightctl/flightctl-ui

Length of output: 710


Validate that valuesYaml parses to a mapping (not scalar/array).

yaml.load() can return scalars or arrays; form validation only checks YAML syntax, not structure. Strings and arrays would pass the Object.keys(values).length > 0 check and be sent to the API as values. Add explicit type validation to ensure only object/mapping types are accepted.

🐛 Proposed fix
    if (app.valuesYaml) {
      try {
        const parsed = yaml.load(app.valuesYaml);
        const isObject = !!parsed && typeof parsed === 'object' && !Array.isArray(parsed);
        if (!isObject) {
          throw new Error('Values content must be a YAML mapping.');
        }
        if (Object.keys(parsed as Record<string, unknown>).length > 0) {
          data.values = parsed as Record<string, unknown>;
        }
      } catch (error) {
        throw new Error('Values content is not valid YAML.');
      }
    }
📝 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.

Suggested change
if (isHelmImageAppForm(app)) {
const data: ImageApplicationProviderSpec & ApplicationProviderSpec = {
name: app.name,
image: app.image,
appType: app.appType,
};
if (app.namespace) {
data.namespace = app.namespace;
}
if (app.valuesYaml) {
try {
const values = yaml.load(app.valuesYaml) as Record<string, unknown>;
if (values && Object.keys(values).length > 0) {
data.values = values;
}
} catch (error) {
throw new Error('Values content is not valid YAML.');
}
}
const fileNames = app.valuesFiles.filter((file) => file && file.trim() !== '');
if (fileNames.length > 0) {
data.valuesFiles = fileNames;
}
return data;
}
if (isHelmImageAppForm(app)) {
const data: ImageApplicationProviderSpec & ApplicationProviderSpec = {
name: app.name,
image: app.image,
appType: app.appType,
};
if (app.namespace) {
data.namespace = app.namespace;
}
if (app.valuesYaml) {
try {
const parsed = yaml.load(app.valuesYaml);
const isObject = !!parsed && typeof parsed === 'object' && !Array.isArray(parsed);
if (!isObject) {
throw new Error('Values content must be a YAML mapping.');
}
if (Object.keys(parsed as Record<string, unknown>).length > 0) {
data.values = parsed as Record<string, unknown>;
}
} catch (error) {
throw new Error('Values content is not valid YAML.');
}
}
const fileNames = app.valuesFiles.filter((file) => file && file.trim() !== '');
if (fileNames.length > 0) {
data.valuesFiles = fileNames;
}
return data;
}
🤖 Prompt for AI Agents
In `@libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts`
around lines 220 - 244, The YAML parsing for Helm app values in the
isHelmImageAppForm branch currently accepts scalars/arrays because it only
checks Object.keys(values). Ensure that after calling yaml.load(app.valuesYaml)
you validate the result is a plain mapping/object (e.g., typeof values ===
'object' && values !== null && !Array.isArray(values')) before assigning to
data.values; if the parsed value is not an object, throw the existing YAML error
(or a new Error('Values content must be a YAML mapping/object.')) so only
object/mapping types are sent to the API and non-mapping YAML is rejected.


const envVars = app.variables.reduce((acc, variable) => {
acc[variable.name] = variable.value;
return acc;
Expand Down Expand Up @@ -265,7 +293,7 @@ export const toAPIApplication = (app: AppForm): ApplicationProviderSpec => {
limits: appLimits,
};
}
return data as ApplicationProviderSpec;
return data;
}

if (isQuadletImageAppForm(app) || isComposeImageAppForm(app)) {
Expand Down Expand Up @@ -433,15 +461,38 @@ const hasApplicationChanged = (currentApp: ApplicationProviderSpec, updatedApp:
return true;
}

if (!areEnvVariablesEqual(currentApp.envVars, updatedApp.variables)) {
return true;
}

// The app is a single container application
if (isSingleContainerAppForm(updatedApp)) {
return hasSingleContainerAppChanged(currentApp, updatedApp);
}

// The app is a Helm application
if (isHelmImageAppForm(updatedApp)) {
const imageApp = currentApp as ImageApplicationProviderSpec;
if (imageApp.image !== updatedApp.image || imageApp.namespace !== updatedApp.namespace) {
return true;
}

// Compare valuesFiles arrays
const currentValuesFiles = (imageApp.valuesFiles || []).filter((file) => file !== '');
const updatedValuesFiles = updatedApp.valuesFiles.filter((file) => file !== '');
if (currentValuesFiles.length !== updatedValuesFiles.length) {
return true;
}
if (!currentValuesFiles.every((file, index) => file === updatedValuesFiles[index])) {
return true;
}
const updatedValues = yaml.load(updatedApp.valuesYaml || ' ') as Record<string, unknown>;
if (JSON.stringify(imageApp.values || {}) !== JSON.stringify(updatedValues)) {
return true;
}
return false;
}

if (!areEnvVariablesEqual(currentApp.envVars, updatedApp.variables)) {
return true;
}
Comment on lines +469 to +494

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the specific file and check the code at lines 469-494
find . -type f -name "deviceSpecUtils.ts" | head -5

Repository: flightctl/flightctl-ui

Length of output: 144


🏁 Script executed:

# Once we locate the file, examine the specific lines
if [ -f "libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts" ]; then
  wc -l "libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts"
fi

Repository: flightctl/flightctl-ui

Length of output: 146


🏁 Script executed:

# Check the yaml library being used and its behavior
grep -r "import.*yaml" libs/ui-components/src/components/Device/EditDeviceWizard/ | head -5

Repository: flightctl/flightctl-ui

Length of output: 700


🏁 Script executed:

# Test yaml.load behavior with empty strings
node <<'EOF'
const yaml = require('js-yaml');

console.log("Testing yaml.load behavior:");
console.log("yaml.load('  '):", yaml.load('  '));
console.log("yaml.load(''):", yaml.load(''));
console.log("yaml.load(null):", yaml.load(null));
console.log("yaml.load(undefined):", yaml.load(undefined));
console.log("typeof yaml.load('  '):", typeof yaml.load('  '));
console.log("yaml.load('  ') === undefined:", yaml.load('  ') === undefined);
console.log("yaml.load('') === undefined:", yaml.load('') === undefined);
EOF

Repository: flightctl/flightctl-ui

Length of output: 903


🏁 Script executed:

sed -n '469,494p' libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts

Repository: flightctl/flightctl-ui

Length of output: 1062


🏁 Script executed:

# Let's also see context around these lines to understand the function
sed -n '450,510p' libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts

Repository: flightctl/flightctl-ui

Length of output: 2365


🏁 Script executed:

# Check js-yaml documentation and behavior - search for how it's used elsewhere
grep -A 5 "yaml.load" libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts | head -20

Repository: flightctl/flightctl-ui

Length of output: 554


🏁 Script executed:

# Let's check the package.json to confirm js-yaml version
grep -A 2 "js-yaml" package.json

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

# Check how imageApp.values is typically set/populated
grep -B 5 -A 5 "imageApp.values" libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts | head -30

Repository: flightctl/flightctl-ui

Length of output: 828


🏁 Script executed:

# Look for where updatedApp.valuesYaml comes from and whether it can be empty
grep -B 3 -A 3 "valuesYaml" libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts | head -40

Repository: flightctl/flightctl-ui

Length of output: 928


🏁 Script executed:

# Check the structure of ImageApplicationProviderSpec and AppForm to understand values field
rg "interface.*ImageApplicationProviderSpec|type.*ImageApplicationProviderSpec" -A 10

Repository: flightctl/flightctl-ui

Length of output: 3106


🏁 Script executed:

# Look for where hasApplicationChanged is called to understand the data flow
rg "hasApplicationChanged" -B 2 -A 2 libs/ui-components/src/components/Device/EditDeviceWizard/

Repository: flightctl/flightctl-ui

Length of output: 1327


🏁 Script executed:

# Check AppForm interface to see valuesYaml type
rg "interface.*AppForm|type.*AppForm" -A 15 | head -50

Repository: flightctl/flightctl-ui

Length of output: 3593


🏁 Script executed:

# See how js-yaml is referenced in package.json
cat package.json | grep -A 5 -B 5 "js-yaml"

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

# Find package.json and check js-yaml version
find . -name "package.json" -type f | head -3

Repository: flightctl/flightctl-ui

Length of output: 155


🏁 Script executed:

# Look at the root package.json
head -50 package.json | grep -A 5 -B 5 "js-yaml" || echo "Not in first 50 lines, checking further"

Repository: flightctl/flightctl-ui

Length of output: 105


🏁 Script executed:

# Search more broadly in package.json
cat package.json | grep "js-yaml"

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

# Let's understand the logic better - look at what happens with imageApp.values
sed -n '1,100p' libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts | head -50

Repository: flightctl/flightctl-ui

Length of output: 1200


🏁 Script executed:

# Check root directory package.json more carefully
cat package.json | grep -C 10 "dependencies\|devDependencies" | grep -A 50 "dependencies" | grep "js-yaml"

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

# Let's look at the actual storage/conversion of valuesYaml to understand the flow better
rg "valuesYaml" libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts -B 5 -A 5

Repository: flightctl/flightctl-ui

Length of output: 1220


🏁 Script executed:

# Check the entire function to see how values comparison works
sed -n '440,520p' libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts

Repository: flightctl/flightctl-ui

Length of output: 2820


🏁 Script executed:

# Let's search for other uses of yaml.load in the codebase to understand behavior patterns
rg "yaml\.load" -B 2 -A 2

Repository: flightctl/flightctl-ui

Length of output: 1668


🏁 Script executed:

# Check how valuesFiles are initialized and used in the YAML editor
rg "valuesFiles" libs/ui-components/src/components/Device/EditDeviceWizard/ -B 3 -A 3

Repository: flightctl/flightctl-ui

Length of output: 7933


🏁 Script executed:

# Let's look at how the form initializes and what happens with empty valuesYaml
sed -n '1,50p' libs/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationHelmForm.tsx

Repository: flightctl/flightctl-ui

Length of output: 2040


🏁 Script executed:

# Check the YamlEditorBase mentioned in the learning to understand empty YAML handling
find . -name "YamlEditorBase.tsx" -type f

Repository: flightctl/flightctl-ui

Length of output: 138


🏁 Script executed:

# Let's trace through - when convertDeviceSpecToForm is called, what's the initial state?
rg "convertDeviceSpecToForm" -A 20 libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts | head -40

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

# Let's check the YamlEditorBase to understand the context from the learning
sed -n '59,64p' libs/ui-components/src/components/common/CodeEditor/YamlEditorBase.tsx

Repository: flightctl/flightctl-ui

Length of output: 215


🏁 Script executed:

# Look at what yaml.load actually returns - let's check if there are any tests or documentation
rg "yaml\.load.*undefined\|undefined.*yaml\.load" -i

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

# Let's verify the issue by looking at a broader context - how values are actually initialized
sed -n '360,395p' libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts

Repository: flightctl/flightctl-ui

Length of output: 1259


🏁 Script executed:

# Let's look at the convertDeviceSpecToForm function more carefully
sed -n '250,320p' libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts

Repository: flightctl/flightctl-ui

Length of output: 1897


🏁 Script executed:

# Let's look at how imageApp.values is initialized/handled in the incoming data
rg "\.values\s*=" libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts | head -15

Repository: flightctl/flightctl-ui

Length of output: 97


🏁 Script executed:

# Check what happens when an app is serialized - does it ever have undefined values?
sed -n '200,250p' libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts

Repository: flightctl/flightctl-ui

Length of output: 1274


🏁 Script executed:

# Let's trace convertDeviceSpecToForm to see how valuesYaml is converted back
rg "convertDeviceSpecToForm" -B 5 -A 30 | grep -A 30 "HelmImageAppForm\|isHelmImageAppForm" | head -50

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

# Let's find where convertDeviceSpecToForm is defined
grep -n "const convertDeviceSpecToForm" libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

# Look at that function
sed -n '120,180p' libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts

Repository: flightctl/flightctl-ui

Length of output: 2072


🏁 Script executed:

# Search for the function that converts DeviceSpec to Form
rg "convertDeviceSpecToForm|convertSpecToForm" -B 2 -A 30 libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts | head -80

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

# Let's search for where the form is initialized from the device spec
rg "HelmImageAppForm|valuesYaml.*=" libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts -B 3 -A 3 | head -80

Repository: flightctl/flightctl-ui

Length of output: 723


🏁 Script executed:

# Let's look for where the form is converted back from the device spec
rg "fromAppProvider|toAppForm" -B 2 -A 30 libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

# Let's search more broadly for form initialization from app spec
rg "imageApp\.values|app\.values" libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts -B 5 -A 5

Repository: flightctl/flightctl-ui

Length of output: 1846


🏁 Script executed:

# Let's understand the full flow - let's see where the ApplicationProviderSpec comes from
rg "imageApp\.values" libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts -B 10 -A 5

Repository: flightctl/flightctl-ui

Length of output: 973


🏁 Script executed:

# Let's verify yaml.load behavior by checking js-yaml documentation or similar code patterns
rg "yaml\." libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts

Repository: flightctl/flightctl-ui

Length of output: 343


🏁 Script executed:

# Let's check if there's a test file for deviceSpecUtils
find . -name "*deviceSpecUtils*.test.ts" -o -name "*deviceSpecUtils*.spec.ts"

Repository: flightctl/flightctl-ui

Length of output: 48


🏁 Script executed:

# Let's check what the actual ImageApplicationProviderSpec structure looks like and how values field is defined
cat libs/types/models/ImageApplicationProviderSpec.ts

Repository: flightctl/flightctl-ui

Length of output: 1258


🏁 Script executed:

# Let's verify the exact logic by reviewing how the comparison works step by step
sed -n '480,490p' libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts

Repository: flightctl/flightctl-ui

Length of output: 430


🏁 Script executed:

# Let's verify the exact type signature for imageApp to understand what imageApp.values could be
rg "type ImageApplicationProviderSpec|interface ImageApplicationProviderSpec" -A 20 libs/types/models/ImageApplicationProviderSpec.ts

Repository: flightctl/flightctl-ui

Length of output: 873


🏁 Script executed:

# Let's check how valuesFiles is handled in the conversion - specifically in both directions
sed -n '115,155p' libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts

Repository: flightctl/flightctl-ui

Length of output: 1588


🏁 Script executed:

# Most importantly, let's see the complete flow - where does the form come from initially?
rg "convertAppProviderSpecToForm|appProviderSpec.*HelmImageAppForm" -B 5 -A 20 libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts

Repository: flightctl/flightctl-ui

Length of output: 48


Fix Helm change detection for empty values and trim valuesFiles consistently.

When updatedApp.valuesYaml is empty or undefined, yaml.load(' ') may return undefined, causing the JSON comparison to incorrectly report changes. Additionally, valuesFiles serialization trims whitespace before filtering, but the comparison does not, creating an inconsistency.

🐛 Proposed fix
-    const currentValuesFiles = (imageApp.valuesFiles || []).filter((file) => file !== '');
-    const updatedValuesFiles = updatedApp.valuesFiles.filter((file) => file !== '');
+    const normalizeFiles = (files: string[]) => files.map((f) => f.trim()).filter(Boolean);
+    const currentValuesFiles = normalizeFiles(imageApp.valuesFiles || []);
+    const updatedValuesFiles = normalizeFiles(updatedApp.valuesFiles);
@@
-    const updatedValues = yaml.load(updatedApp.valuesYaml || '  ') as Record<string, unknown>;
-    if (JSON.stringify(imageApp.values || {}) !== JSON.stringify(updatedValues)) {
+    const updatedValuesRaw = yaml.load(updatedApp.valuesYaml || '') as unknown;
+    const updatedValues =
+      updatedValuesRaw && typeof updatedValuesRaw === 'object' && !Array.isArray(updatedValuesRaw)
+        ? (updatedValuesRaw as Record<string, unknown>)
+        : {};
+    if (JSON.stringify(imageApp.values || {}) !== JSON.stringify(updatedValues)) {
       return true;
     }
🤖 Prompt for AI Agents
In `@libs/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts`
around lines 469 - 494, The Helm-change detection incorrectly treats empty
valuesYaml as a change and inconsistently handles whitespace in valuesFiles; in
isHelmImageAppForm branch, ensure valuesFiles are trimmed before filtering by
replacing (imageApp.valuesFiles || []) and updatedApp.valuesFiles with trimmed
versions (e.g., map(file => file.trim())), then compare lengths and entries; for
valuesYaml use yaml.load(updatedApp.valuesYaml || '{}') (or coerce undefined to
an empty object) and guard the parsed result so that
JSON.stringify(imageApp.values || {}) is compared against
JSON.stringify(parsedValues || {}); keep the rest of the early returns, and
leave areEnvVariablesEqual usage unchanged for non-Helm apps.


// The app is an image application (Quadlet/Compose image apps)
if (isCurrentImageApp) {
const imageApp = currentApp as ImageApplicationProviderSpec;
Expand Down Expand Up @@ -595,8 +646,8 @@ export const getApplicationValues = (deviceSpec?: DeviceSpec): AppForm[] => {
}) || [];

return {
specType: AppSpecType.OCI_IMAGE,
appType: AppType.AppTypeContainer,
specType: AppSpecType.OCI_IMAGE,
name: app.name || '',
image: app.image,
variables: getAppFormVariables(app),
Expand All @@ -611,23 +662,36 @@ export const getApplicationValues = (deviceSpec?: DeviceSpec): AppForm[] => {
};
}

// Helm application
if (app.appType === AppType.AppTypeHelm && isImageAppProvider(app)) {
return {
appType: AppType.AppTypeHelm,
specType: AppSpecType.OCI_IMAGE,
name: app.name || '',
image: app.image,
namespace: app.namespace,
valuesYaml: app.values && Object.keys(app.values).length > 0 ? yaml.dump(app.values) : undefined,
valuesFiles: app.valuesFiles || [''],
};
}

// Compose or Quadlet image application
if (isImageAppProvider(app)) {
return {
appType: app.appType,
specType: AppSpecType.OCI_IMAGE,
name: app.name || '',
image: app.image,
appType: app.appType,
variables: getAppFormVariables(app),
volumes: convertVolumesToForm(app.volumes),
};
} as QuadletImageAppForm | ComposeImageAppForm;
}

// Compose or Quadlet inline application
const inlineApp = app as InlineApplicationProviderSpec;
return {
specType: AppSpecType.INLINE,
appType: app.appType,
specType: AppSpecType.INLINE,
name: app.name || '',
files: inlineApp.inline,
variables: getAppFormVariables(app),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,15 @@
border: 1px solid gray !important;
margin-top: 1rem;
}

/*
The tooltip text has the same color as the background, making the text unreadable in light and dark modes.
Due to Patternfly targets the tooltip content ([class*='pf-v6-c-content']), just using the CSS variables does not work.
*/
.fctl-application-helm-form__tooltip [class*='pf-v6-c-content'] {
color: var(--pf-t--global--text--color--300, white);
}

.pf-v6-theme-dark .fctl-application-helm-form__tooltip [class*='pf-v6-c-content'] {
color: var(--pf-t--global--text--color--100, gray);
}
Loading