-
-
setFilter(event.target.value)} placeholder="Filter…" value={filter} />
-
-
-
+ {controlsPlacement === 'internal' ? (
+
+
+
setInternalFilter(event.target.value)} placeholder="Filter…" value={internalFilter} />
+
+
+
+
+
-
-
+ ) : null}
{effective.isLoading ? : (
@@ -99,7 +117,6 @@ export function ConfigTreeView({ owner }: ConfigTreeViewProps) {
collapsed={collapsed}
key={node.kind === 'group' ? node.prefix : node.entry.id}
node={node}
- onDelete={setDeletingEntry}
onEdit={setEditingEntry}
onSelect={setSelectedEntryId}
selectedEntryId={selectedEntryId}
@@ -117,17 +134,15 @@ export function ConfigTreeView({ owner }: ConfigTreeViewProps) {
!open && setEditingEntry(undefined)} open={Boolean(editingEntry)} ownerId={owner.id} ownerType={ownerType} />
- !open && setDeletingEntry(undefined)} open={Boolean(deletingEntry)} ownerId={owner.id} ownerType={ownerType} />
);
-}
+});
interface TreeRowProps {
collapsed: Set
;
depth?: number;
node: TreeNode;
- onDelete: (entry: ConfigEntry) => void;
onEdit: (entry: ConfigEntry) => void;
onSelect: (id: string) => void;
selectedEntryId: null | string;
@@ -135,7 +150,7 @@ interface TreeRowProps {
sourceById: Map;
}
-function TreeRow({ collapsed, depth = 0, node, onDelete, onEdit, onSelect, selectedEntryId, setCollapsed, sourceById }: TreeRowProps) {
+function TreeRow({ collapsed, depth = 0, node, onEdit, onSelect, selectedEntryId, setCollapsed, sourceById }: TreeRowProps) {
if (node.kind === 'group') {
const isCollapsed = collapsed.has(node.prefix);
const segmentName = lastSegment(node.prefix);
@@ -162,7 +177,6 @@ function TreeRow({ collapsed, depth = 0, node, onDelete, onEdit, onSelect, selec
depth={depth + 1}
key={child.kind === 'group' ? child.prefix : child.entry.id}
node={child}
- onDelete={onDelete}
onEdit={onEdit}
onSelect={onSelect}
selectedEntryId={selectedEntryId}
@@ -206,7 +220,6 @@ function TreeRow({ collapsed, depth = 0, node, onDelete, onEdit, onSelect, selec
{isInherited ? : (
-
)}
diff --git a/src/GroundControl.Tower/src/components/tower/config/DeleteEntryDialog.tsx b/src/GroundControl.Tower/src/components/tower/config/DeleteEntryDialog.tsx
index bf5ba167..a9762848 100644
--- a/src/GroundControl.Tower/src/components/tower/config/DeleteEntryDialog.tsx
+++ b/src/GroundControl.Tower/src/components/tower/config/DeleteEntryDialog.tsx
@@ -4,6 +4,7 @@ import { useDeleteEntry, type ConfigEntry, type ConfigEntryOwnerType } from '@/q
interface DeleteEntryDialogProps {
entry?: ConfigEntry;
+ onDeleted?: () => void;
onOpenChange: (open: boolean) => void;
open: boolean;
ownerId?: string;
@@ -11,7 +12,7 @@ interface DeleteEntryDialogProps {
projectId?: string;
}
-export function DeleteEntryDialog({ entry, onOpenChange, open, ownerId, ownerType = 1, projectId }: DeleteEntryDialogProps) {
+export function DeleteEntryDialog({ entry, onDeleted, onOpenChange, open, ownerId, ownerType = 1, projectId }: DeleteEntryDialogProps) {
const resolvedOwnerId = ownerId ?? projectId ?? '';
const deleteEntry = useDeleteEntry(resolvedOwnerId, ownerType);
@@ -22,6 +23,7 @@ export function DeleteEntryDialog({ entry, onOpenChange, open, ownerId, ownerTyp
await deleteEntry.mutateAsync({ id: entry.id, version: entry.version.toString() });
onOpenChange(false);
+ onDeleted?.();
}
return (
diff --git a/src/GroundControl.Tower/src/components/tower/config/EntryModal.test.tsx b/src/GroundControl.Tower/src/components/tower/config/EntryModal.test.tsx
index f78abefa..39554b0f 100644
--- a/src/GroundControl.Tower/src/components/tower/config/EntryModal.test.tsx
+++ b/src/GroundControl.Tower/src/components/tower/config/EntryModal.test.tsx
@@ -15,6 +15,7 @@ vi.mock('@/queries/useConfigEntries', async (importOriginal) => {
return {
...actual,
useCreateEntry: () => ({ isPending: false, mutateAsync: vi.fn() }),
+ useDeleteEntry: () => ({ isPending: false, mutateAsync: vi.fn() }),
useUpdateEntry: () => ({ isPending: false, mutateAsync: vi.fn() }),
};
});
@@ -91,4 +92,17 @@ describe('EntryModal', () => {
expect(defaultValueInput.value).toBe('real-secret');
expect(screen.getByRole('button', { name: 'Save entry' })).not.toBeDisabled();
});
+
+ it('shows delete entry actions inside edit mode', async () => {
+ const user = userEvent.setup();
+
+ renderWithClient(
+
,
+ );
+
+ await user.click(screen.getByRole('button', { name: 'Delete entry' }));
+
+ expect(await screen.findByRole('heading', { name: 'Delete Entry' })).toBeInTheDocument();
+ expect(screen.getByText(/Secret:ApiKey/)).toBeInTheDocument();
+ });
});
diff --git a/src/GroundControl.Tower/src/components/tower/config/EntryModal.tsx b/src/GroundControl.Tower/src/components/tower/config/EntryModal.tsx
index 1892e9b2..51bf275f 100644
--- a/src/GroundControl.Tower/src/components/tower/config/EntryModal.tsx
+++ b/src/GroundControl.Tower/src/components/tower/config/EntryModal.tsx
@@ -1,6 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
-import { useEffect, useMemo } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
@@ -12,6 +12,7 @@ import { Textarea } from '@/components/ui/textarea';
import { ScopedValuesField } from '@/components/tower/data/ScopedValuesField';
import { getConfigEntry } from '@/api/endpoints/config-entries';
import { useCreateEntry, useUpdateEntry, type ConfigEntry, type ConfigEntryOwnerType } from '@/queries/useConfigEntries';
+import { DeleteEntryDialog } from './DeleteEntryDialog';
const SENSITIVE_MASK = '***';
const valueTypes = ['String', 'Int32', 'Int64', 'Double', 'Decimal', 'Boolean', 'DateTime', 'DateTimeOffset', 'DateOnly', 'TimeOnly'] as const;
@@ -43,6 +44,7 @@ export function EntryModal({ entry, mode, onOpenChange, open, ownerId, ownerType
const resolvedOwnerId = ownerId ?? projectId ?? '';
const createEntry = useCreateEntry(resolvedOwnerId, ownerType);
const updateEntry = useUpdateEntry(resolvedOwnerId, ownerType);
+ const [confirmingDelete, setConfirmingDelete] = useState(false);
const formValues = useMemo(() => toFormValues(entry), [entry]);
const form = useForm
({
defaultValues: formValues,
@@ -90,6 +92,7 @@ export function EntryModal({ entry, mode, onOpenChange, open, ownerId, ownerType
useEffect(() => {
if (!open) {
+ setConfirmingDelete(false);
return;
}
@@ -194,11 +197,27 @@ export function EntryModal({ entry, mode, onOpenChange, open, ownerId, ownerType
watch={form.watch}
/>
-
-
+
+ {isEdit && entry ? (
+
+ ) : null}
+
+
+
+
+
+ onOpenChange(false)}
+ onOpenChange={setConfirmingDelete}
+ open={confirmingDelete}
+ ownerId={ownerId}
+ ownerType={ownerType}
+ projectId={projectId}
+ />
);
}
diff --git a/src/GroundControl.Tower/src/components/tower/shell/Sidebar.tsx b/src/GroundControl.Tower/src/components/tower/shell/Sidebar.tsx
index d9a0310f..1eb44dc1 100644
--- a/src/GroundControl.Tower/src/components/tower/shell/Sidebar.tsx
+++ b/src/GroundControl.Tower/src/components/tower/shell/Sidebar.tsx
@@ -43,7 +43,7 @@ export function Sidebar({ className, onNavigate }: SidebarProps) {
const pathname = useRouterState({ select: (state) => state.location.pathname });
return (
-