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
57 changes: 57 additions & 0 deletions src/components/configuration/ConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
tombstoneFieldProfileValueFn,
bulkSaveProfileValuesFn,
getBatchFieldProfilesFn,
getBaseConfigOverridesFn,
availableScopesOptions,
resetBaseConfigFieldFn,
getResolvedConfigFn,
Expand Down Expand Up @@ -38,12 +39,15 @@ import {
buildSavePayload,
mergeIndexedArrayEdits,
partitionScopeResetPaths,
collectEntryOverrideKeys,
executeEntryOverridesReset,
} from './utils';
import { validateMcpCrossField } from './sections/McpServersRenderer';
import { ScopeSelector, ScopeTriggerButton } from './ScopeSelector';
import { StickyActionBar } from '@/components/shared';
import { ConfigTableOfContents } from './ConfigTableOfContents';
import { ResetBaseConfigDialog } from './ResetBaseConfigDialog';
import { ResetOverridesDialog } from './ResetOverridesDialog';
import { ConfirmSaveDialog } from './ConfirmSaveDialog';
import { ConfigTabContent } from './ConfigTabContent';
import { ImportYamlDialog } from './ImportYamlDialog';
Expand Down Expand Up @@ -152,9 +156,15 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi
if (yamlMcpKeys && Array.isArray(yamlMcpKeys)) {
result.mcpServers = new Set(yamlMcpKeys);
}
const yamlCustomEndpointKeys = baseConfigData?.yamlCustomEndpointKeys;
if (yamlCustomEndpointKeys && Array.isArray(yamlCustomEndpointKeys)) {
result.endpoints = new Set(yamlCustomEndpointKeys);
}
return result;
}, [baseConfigData]);

const dbOverrideKeys = useMemo(() => collectEntryOverrideKeys(dbOverrides), [dbOverrides]);

const hasUnmappedSections = useMemo(
() =>
schemaTree.some(
Expand Down Expand Up @@ -536,6 +546,39 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi
});
}, []);

const [entryResetTarget, setEntryResetTarget] = useState<t.EntryResetTarget | null>(null);
const [entryResetting, setEntryResetting] = useState(false);
const [entryResetError, setEntryResetError] = useState<string | null>(null);

const handleEntryOverridesReset = useCallback((target: t.EntryResetTarget) => {
setEntryResetError(null);
setEntryResetTarget(target);
}, []);

const handleEntryResetConfirm = useCallback(async () => {
if (!entryResetTarget || entryResetting) return;
setEntryResetting(true);
setEntryResetError(null);
try {
await executeEntryOverridesReset(entryResetTarget, schemaPathSet, {
fetchOverrides: () => getBaseConfigOverridesFn().then((r) => r.overrides),
resetField: (fieldPath) => resetBaseConfigFieldFn({ data: { fieldPath } }),
saveEntries: (entries) => saveBaseConfigFn({ data: { entries } }),
});
await queryClient.invalidateQueries({ queryKey: ['baseConfig'] });
/** Remount session-keyed field state (e.g. a SecretField opened for Replace but left untouched, which no invalidation reaches) so nothing local survives past the reset. */
setEditSessionId((id) => id + 1);
setEntryResetting(false);
setEntryResetTarget(null);
Comment thread
dustinhealy marked this conversation as resolved.
notifySuccess(localize('com_config_reset_entry_success', { name: entryResetTarget.label }));
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
setEntryResetting(false);
setEntryResetError(message);
notifyError(message);
}
}, [entryResetTarget, entryResetting, schemaPathSet, queryClient, localize]);

const handleConfirmSave = useCallback(async () => {
if (saving) return;
const { touched, saves, resets } = buildSavePayload(touchedPaths, editedValues, schemaPathSet);
Expand Down Expand Up @@ -1006,6 +1049,8 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi
showConfiguredOnly={showConfiguredOnly}
isEditingScope={isEditingScope}
baseRecordKeys={baseRecordKeys}
dbOverrideKeys={isEditingScope ? undefined : dbOverrideKeys}
onResetEntryOverrides={isEditingScope ? undefined : handleEntryOverridesReset}
onValidationError={(message) => notifyError(message)}
editSessionId={editSessionId}
/>
Expand Down Expand Up @@ -1068,6 +1113,18 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi
setResetBaseError(null);
}}
/>

<ResetOverridesDialog
target={entryResetTarget}
resetting={entryResetting}
error={entryResetError}
onConfirm={handleEntryResetConfirm}
onCancel={() => {
if (entryResetting) return;
setEntryResetTarget(null);
setEntryResetError(null);
}}
/>
</div>
);
}
Expand Down
4 changes: 4 additions & 0 deletions src/components/configuration/ConfigTabContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export function ConfigTabContent({
showConfiguredOnly,
isEditingScope,
baseRecordKeys,
dbOverrideKeys,
onResetEntryOverrides,
onValidationError,
editSessionId,
}: t.ConfigTabContentProps) {
Expand Down Expand Up @@ -188,6 +190,8 @@ export function ConfigTabContent({
showConfiguredOnly,
isEditingScope,
yamlBaseKeys: baseRecordKeys?.[dataKey],
dbOverrideKeys: dbOverrideKeys?.[dataKey],
onResetEntryOverrides,
onValidationError,
editSessionId,
};
Expand Down
62 changes: 62 additions & 0 deletions src/components/configuration/ResetOverridesDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Button, Dialog } from '@clickhouse/click-ui';
import type * as t from '@/types';
import { useLocalize } from '@/hooks';

export function ResetOverridesDialog({
target,
resetting,
error,
onConfirm,
onCancel,
}: t.ResetOverridesDialogProps) {
const localize = useLocalize();

return (
<Dialog
open={target != null}
onOpenChange={(isOpen) => {
if (!isOpen) onCancel();
}}
>
<Dialog.Content
title={localize('com_config_reset_entry_title')}
showClose
onClose={onCancel}
className="modal-frost"
>
<div className="flex flex-col gap-4">
<p className="text-sm leading-relaxed text-(--cui-color-text-default)">
{localize('com_config_reset_entry_confirm', { name: target?.label ?? '' })}
</p>
{error && (
<div
className="rounded-lg bg-[rgba(220,38,38,0.1)] px-3 py-2 text-sm font-medium text-(--cui-color-text-danger)"
role="alert"
>
{error}
</div>
)}
<div className="flex items-center justify-end gap-2">
<Button
type="secondary"
label={localize('com_ui_cancel')}
onClick={onCancel}
disabled={resetting}
/>
<Button
type="danger"
label={
resetting
? localize('com_config_reset_base_resetting')
: localize('com_config_reset_entry_action')
}
iconLeft={resetting ? 'loading-animated' : undefined}
onClick={onConfirm}
disabled={resetting}
/>
</div>
</div>
</Dialog.Content>
</Dialog>
);
}
39 changes: 24 additions & 15 deletions src/components/configuration/fields/ArrayObjectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function ArrayObjectField({
addTriggerRef,
renderFields,
entryIdPrefix,
entryControls,
editSessionId,
}: t.ArrayObjectFieldProps) {
const localize = useLocalize();
Expand Down Expand Up @@ -104,21 +105,29 @@ export function ArrayObjectField({
onClick={handleAdd}
/>
)}
{items.map((item, index) => (
<ObjectEntryCard
key={keys[index] ?? index}
id={entryIdPrefix ? `${entryIdPrefix}-${index}` : undefined}
entryKey={getEntryLabel(item) ?? localize('com_config_entry_n', { n: String(index + 1) })}
fields={fields}
value={item}
onValueChange={(v) => handleEntryChange(index, v)}
onRemove={disabled ? undefined : () => handleRemove(index)}
disabled={disabled}
defaultExpanded={keys[index] === expandedKeyRef.current}
renderFields={renderFields}
editSessionId={editSessionId}
/>
))}
{items.map((item, index) => {
const controls = entryControls?.(index, item);
return (
<ObjectEntryCard
key={keys[index] ?? index}
id={entryIdPrefix ? `${entryIdPrefix}-${index}` : undefined}
entryKey={
getEntryLabel(item) ?? localize('com_config_entry_n', { n: String(index + 1) })
}
fields={fields}
value={item}
onValueChange={(v) => handleEntryChange(index, v)}
onRemove={
disabled || controls?.canRemove === false ? undefined : () => handleRemove(index)
}
resetOverrides={controls?.resetOverrides}
disabled={disabled}
defaultExpanded={keys[index] === expandedKeyRef.current}
renderFields={renderFields}
editSessionId={editSessionId}
/>
);
})}
{items.length === 0 && !hideAddButton && (
<p className="py-2 text-sm text-(--cui-color-text-muted)">
{localize('com_config_no_entries')}
Expand Down
21 changes: 20 additions & 1 deletion src/components/configuration/fields/ObjectEntryCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Icon } from '@clickhouse/click-ui';
import { Icon, IconButton } from '@clickhouse/click-ui';
import { useState, useCallback, useRef, useEffect } from 'react';
import type * as t from '@/types';
import { TrashButton } from '@/components/shared';
Expand All @@ -14,6 +14,7 @@ export function ObjectEntryCard({
onValueChange,
onRemove,
onRename,
resetOverrides,
disabled,
defaultExpanded = false,
renderFields,
Expand Down Expand Up @@ -191,6 +192,24 @@ export function ObjectEntryCard({
<span>{localize('com_config_add_field')}</span>
</button>
)}
{!disabled && resetOverrides && (
<span
className="opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
onClick={(e) => e.stopPropagation()}
title={resetOverrides.title}
>
<IconButton
icon="refresh"
size="sm"
onClick={(e) => {
e.preventDefault();
resetOverrides.onClick();
}}
disabled={resetOverrides.disabled}
aria-label={localize('com_a11y_reset_entry_overrides', { name: entryKey })}
/>
</span>
)}
{!disabled && onRemove && (
<span
className="opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
Expand Down
Loading