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
17 changes: 13 additions & 4 deletions src/lib/components/layout/ClusterSwitcher.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,16 @@
const selectedCluster = $derived(
availableClusters.find((cluster) => cluster.id === currentCluster) ?? null
);
let selectingClusterId = $state<string | null>(null);

function selectCluster(clusterId: string) {
if (clusterId === currentCluster) return;
clusterStore.setCluster(clusterId);
async function selectCluster(clusterId: string) {
if (clusterId === currentCluster || selectingClusterId) return;
selectingClusterId = clusterId;
try {
await clusterStore.setCluster(clusterId);
} finally {
selectingClusterId = null;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</script>

Expand Down Expand Up @@ -93,6 +99,7 @@
onSelect={() => selectCluster(cluster.id)}
class={cn(
'mb-0.5 cursor-pointer gap-3 rounded-xl px-3 py-3 transition-colors last:mb-0',
selectingClusterId ? 'pointer-events-none opacity-60' : '',
cluster.id === currentCluster
? 'bg-primary/5 hover:bg-primary/10'
: 'hover:bg-accent/50'
Expand Down Expand Up @@ -133,7 +140,9 @@
{eventsStore.clusterUnreadCounts[cluster.id]}
</span>
{/if}
{#if cluster.id === currentCluster}
{#if selectingClusterId === cluster.id}
<Icon name="loader-2" size={12} class="animate-spin text-muted-foreground" />
{:else if cluster.id === currentCluster}
<Icon name="check" size={12} class="text-green-500" />
{/if}
</DropdownMenu.Item>
Expand Down
147 changes: 127 additions & 20 deletions src/lib/components/wizards/ResourceWizard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -87,21 +87,21 @@
const values: Record<string, unknown> = {};

template.fields.forEach((field) => {
const path = field.path.split('.');
let current = parsed;
for (let i = 0; i < path.length; i++) {
if (!current) break;
if (i === path.length - 1) {
values[field.name] = coerceFieldValue(field, current[path[i]]);
} else {
current = current[path[i]] as Record<string, unknown>;
}
}
values[field.name] = coerceFieldValue(field, getValueAtPath(parsed, field.path));

// Apply default namespace
if (field.name === 'namespace' && defaultNamespace) {
values[field.name] = defaultNamespace;
}

if (field.virtual) {
const manifestValue = inferVirtualFieldValue(field, parsed);
if (manifestValue !== undefined) {
values[field.name] = manifestValue;
} else if (values[field.name] === undefined && field.default !== undefined) {
values[field.name] = field.default;
}
}
});
formValues = values;
hasInitializedFormValues = true;
Expand All @@ -128,9 +128,28 @@
template.fields.forEach((field) => {
if (field.virtual) return;

if (!shouldShowField(field)) {
const visibleFieldWithSamePath = template.fields.some(
(candidate) =>
candidate !== field &&
!candidate.virtual &&
candidate.path === field.path &&
shouldShowField(candidate)
);
if (!visibleFieldWithSamePath) {
doc.deleteIn(field.path.split('.'));
}
return;
}

const value = coerceFieldValue(field, formValues[field.name]);
const path = field.path.split('.');

if (field.name === 'verifyMode' && value === '') {
doc.deleteIn(path.slice(0, -1));
return;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (field.type === 'number' && value === undefined) {
doc.deleteIn(path);
return;
Expand All @@ -153,18 +172,17 @@
const values: Record<string, unknown> = { ...formValues };

template.fields.forEach((field) => {
if (field.virtual) return;

const path = field.path.split('.');
let current = parsed;
for (let i = 0; i < path.length; i++) {
if (!current) break;
if (i === path.length - 1) {
values[field.name] = coerceFieldValue(field, current[path[i]]);
} else {
current = current[path[i]] as Record<string, unknown>;
if (field.virtual) {
const manifestValue = inferVirtualFieldValue(field, parsed);
if (manifestValue !== undefined) {
values[field.name] = manifestValue;
} else if (values[field.name] === undefined && field.default !== undefined) {
values[field.name] = field.default;
}
return;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

values[field.name] = coerceFieldValue(field, getValueAtPath(parsed, field.path));
});
formValues = values;
} catch (err) {
Expand All @@ -176,6 +194,44 @@
}
}

function getValueAtPath(source: Record<string, unknown>, path: string): unknown {
const segments = path.split('.');
let current: unknown = source;

for (const segment of segments) {
if (!current || typeof current !== 'object') {
return undefined;
}
current = (current as Record<string, unknown>)[segment];
}

return current;
}

function hasPopulatedValue(value: unknown): boolean {
return (
value !== undefined &&
value !== null &&
value !== '' &&
(!Array.isArray(value) || value.length > 0)
);
}

function inferVirtualFieldValue(
field: TemplateField,
source: Record<string, unknown>
): string | undefined {
for (const candidate of template.fields) {
if (candidate.virtual || candidate.showIf?.field !== field.name) continue;
if (!hasPopulatedValue(getValueAtPath(source, candidate.path))) continue;

const showIfValue = candidate.showIf.value;
return Array.isArray(showIfValue) ? showIfValue[0] : showIfValue;
}

return undefined;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function coerceFieldValue(field: TemplateField, value: unknown): unknown {
if (field.type !== 'number') {
return value;
Expand Down Expand Up @@ -431,10 +487,61 @@
}
});

const resourceConflict = validateHelmReleaseResourceValues();
if (resourceConflict) {
for (const fieldName of [
'resourceLimitsCpu',
'resourceLimitsMemory',
'resourceRequestsCpu',
'resourceRequestsMemory'
]) {
if (formValues[fieldName]) {
errors[fieldName] = resourceConflict;
}
}
}

validationErrors = errors;
return Object.keys(errors).length === 0;
}

function validateHelmReleaseResourceValues(): string | null {
if (template.kind !== 'HelmRelease') return null;

const structuredResourceFields = [
'resourceLimitsCpu',
'resourceLimitsMemory',
'resourceRequestsCpu',
'resourceRequestsMemory'
];
if (!structuredResourceFields.some((fieldName) => Boolean(formValues[fieldName]))) {
return null;
}

const values = formValues.values;
if (!values) return null;

try {
const parsedValues =
typeof values === 'string'
? (parse(values) as Record<string, unknown> | null)
: (values as Record<string, unknown>);
if (
parsedValues &&
typeof parsedValues === 'object' &&
!Array.isArray(parsedValues) &&
'resources' in parsedValues
) {
return 'Remove resources from Values before using structured resource fields.';
}
} catch (err) {
logger.warn(err, 'Failed to parse HelmRelease values while checking resource conflicts');
return null;
}

return null;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Check if form is valid (derived)
const isFormValid = $derived.by(() => {
if (yamlError) return false; // Invalid if YAML has syntax errors
Expand Down
Loading
Loading