Make service connection config editable on service page
The service detail page showed non-secret connection config (base_url, user_id, username, timeout_seconds) as read-only. Render schema-driven editable inputs (reusing the create-dialog pattern) with a draftConfig state hydrated from the instance, and unify the save button to persist both config and secrets. Number fields render as type=number; the base_url schema description surfaces as helper text.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
dir: frontend/src/pages
|
||||
|
||||
## role
|
||||
Top-level page components for the application's main navigation routes, each encapsulating a full-feature UI for managing services, media, files, users, settings, and system actions.
|
||||
Top-level page components constituting the primary UI screens/routes of the frontend application, covering dashboards, media management, file browsing, services, settings, users, and task automation.
|
||||
## parent
|
||||
index: frontend/src/.pi-map.index.md
|
||||
map: frontend/src/.pi-map.md
|
||||
|
||||
@@ -4,7 +4,7 @@ dir: frontend/src/pages
|
||||
index: frontend/src/pages/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Top-level page components for the application's main navigation routes, each encapsulating a full-feature UI for managing services, media, files, users, settings, and system actions.
|
||||
Top-level page components constituting the primary UI screens/routes of the frontend application, covering dashboards, media management, file browsing, services, settings, users, and task automation.
|
||||
## files
|
||||
- Actions.tsx | Provides a React component for managing reusable server tasks (shell/python actions) with CRUD operations, service selection, and execution history display. | exp: func:Actions(), call:useServiceInstances, call:useTasks, call:useSaveTask, call:useDeleteTask, call:useRunTask, call:useState, call:emptyTask, call:useMemo, call:tasks.find, call:useTaskRuns, call:setDraft, call:setDraftBaseline, call:setEditOpen, call:setRunServiceId, call:saveTask.mutateAsync, call:setTab, call:String, call:tasks.map, call:openEdit, call:initialFromTask, call:runTask.mutateAsync, call:sshServices.map, call:selectedRuns.data.items.map, call:new Date(run.created_at * 1000).toLocaleString, call:deleteTask.mutate | dep: react, ../types, ../hooks/useSettings, ../hooks/useServices, ../components/DialogFooter, ../components/HoverEditButton, ../components/SectionCard, ../components/SelectionRailCard, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/card, @/components/ui/dialog, @/components/ui/input, @/components/ui/label, @/components/ui/select, @/components/ui/separator, @/components/ui/tabs, @/components/ui/textarea
|
||||
- Applications.tsx | Renders a tabbed Applications dashboard with Jellyfin library statistics and media management, plus a placeholder for future Nextcloud support. | exp: func:Applications(), call:useState | dep: react, react-router-dom, @/components/ui/alert, @/components/ui/badge, @/components/ui/tabs, ./Media, ../hooks/useDashboard, ../hooks/useServices, ../components/SectionCard, ../components/TabbedCard
|
||||
@@ -12,15 +12,15 @@ Top-level page components for the application's main navigation routes, each enc
|
||||
- FileBrowser.impl.tsx | A React component that implements a file browser with directory listing, file selection, ffprobe media metadata inspection, and job execution capabilities. | exp: func:FileBrowser(), call:useSearchParams, call:useState, call:useMonitoringSettings, call:useMemo, call:(machines ?? []).filter, call:machine.services.includes, call:searchParams.get, call:usePersistentState, call:isVideoFile, call:requestedPath.includes, call:requestedPath.replace, call:selectedPath.replace, call:defaultFileBrowserState, call:useNavigate, call:setBrowserState, call:useDirectoryListing, call:useFfprobe, call:useJobTemplates, call:useRunJob, call:updateBrowserState, call:setSearchParams, call:next.set, call:next.delete, call:navigate, call:currentDir.replace, call:rows.push, call:entry.name.split(".").pop, call:formatSize, call:formatTime, call:updater, call:Object.keys(next).filter, call:rows.find, call:templates?.find, call:fileMachines.map, call:refetch, call:String, call:templates.map, call:runJob.mutate, call:navigateToSettings | dep: react, react-router-dom, @tanstack/react-table, @/components/ui/data-table, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/card, @/components/ui/input, @/components/ui/label, @/components/ui/select, @/components/ui/tabs, ../hooks/useFiles, ../hooks/usePersistentState, ../hooks/useSettings, ../components/SectionCard, ../components/TabbedCard
|
||||
- FileBrowser.tsx | Re-exports the FileBrowser component from its implementation file | dep: ./FileBrowser.impl
|
||||
- Media.tsx | A React component for managing and browsing Jellyfin media libraries with server-driven pagination, index building controls, and responsive data table display. | exp: func:Media(), call:useNavigate, call:useSearchParams, call:usePrefersSmallScreen, call:useServiceInstances, call:searchParams.get, call:jellyfinServices.find, call:useCounts, call:useLibraries, call:useMediaStatus, call:useBuildIndex, call:useStopBuildIndex, call:useForceStopBuildIndex, call:usePersistentState, call:defaultMediaTabState, call:setMediaState, call:useState, call:useEffect, call:setSearchParams, call:next.set, call:useMediaDataQuery, call:Math.floor, call:updater, call:useMemo, call:navigate, call:encodeURIComponent, call:Math.max, call:Math.ceil, call:formatDuration, call:jellyfinServices.map, call:status.item_count.toLocaleString, call:counts.movies.toLocaleString, call:counts.series.toLocaleString, call:counts.episodes.toLocaleString, call:(libraries?.length ?? 0).toLocaleString, call:buildIndex.mutate, call:stopBuildIndex.mutate, call:forceStopBuildIndex.mutate, call:Math.round, call:status?.build_items_processed?.toLocaleString, call:status?.build_items_total?.toLocaleString, call:status?.build_library_items_processed?.toLocaleString, call:status?.build_library_items_total?.toLocaleString, call:updateMediaState, call:total.toLocaleString | dep: react, react-router-dom, @tanstack/react-table, @/components/ui/data-table, @/components/ui/alert, @/components/ui/button, @/components/ui/card, @/components/ui/input, @/components/ui/label, @/components/ui/progress, @/components/ui/select, ../hooks/useMedia, ../hooks/usePersistentState, ../types, ../hooks/useServices, ../hooks/useDashboard, @/components/ui (data-table, alert, button, card, input, label, progress, select)
|
||||
- ServicePage.tsx | Provides a UI for viewing and editing a service instance's configuration, secrets, and widget bindings, with save and delete functionality. | exp: func:ServicePage(), call:useParams, call:useServiceInstances, call:useSaveServiceInstance, call:useDeleteServiceInstance, call:useMemo, call:services.find, call:getServiceBinding, call:useState, call:setName, call:setEnabled, call:setHydrated, call:saveService.mutateAsync, call:buildInput, call:setDeleteOpen, call:binding.widgets.map, call:deleteService.mutate | dep: react, react-router-dom, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/input, @/components/ui/label, @/components/ui/switch, ../hooks/useServices, ../types, ../components/SectionCard, ../components/ConfirmDialog, ../integrations/registry, @/components/ui/*
|
||||
- ServicePage.tsx | Provides a settings form to view, edit (config and secrets), save, and delete a specific service instance, and lists its available widgets. | exp: func:ServicePage(), call:useParams, call:useServiceInstances, call:useServiceTypes, call:useSaveServiceInstance, call:useDeleteServiceInstance, call:useMemo, call:services.find, call:getServiceBinding, call:types.find, call:useState, call:setName, call:setEnabled, call:setDraftConfig, call:setHydrated, call:saveService.mutateAsync, call:buildInput, call:setDeleteOpen, call:binding.widgets.map, call:deleteService.mutate | dep: react, react-router-dom, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/input, @/components/ui/label, @/components/ui/switch, ../hooks/useServices, ../types, ../components/SectionCard, ../components/ConfirmDialog, ../integrations/registry, @/components/ui (alert, badge, button, input, label, switch)
|
||||
- ServicesPage.tsx | A React page component for managing external service instances, allowing users to view, create, and delete service configurations with dynamic form fields based on service type schemas. | exp: func:ServicesPage(), call:useNavigate, call:useServiceInstances, call:useServiceTypes, call:useDeleteServiceInstance, call:useState, call:useMemo, call:map.get, call:list.push, call:map.set, call:[...map.entries()].sort, call:map.entries, call:a[0].localeCompare, call:types.find, call:getServiceBinding, call:setCreateOpen, call:grouped.map, call:typeName, call:instances.map, call:Object.entries(s.secrets_set).some, call:navigate, call:setDeleteId, call:Boolean, call:deleteService.mutate | dep: react, react-router-dom, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/input, @/components/ui/label, @/components/ui/switch, @/components/ui/dialog, lucide-react, ../hooks/useServices, ../types, ../components/SectionCard, ../components/ConfirmDialog, ../components/DialogFooter, ../integrations/registry, @/components/ui (alert, badge, button, input, label, switch, dialog)
|
||||
- Settings.tsx | This file provides the UI components and form logic for managing application settings, including configuring monitoring machines via SSH and managing SSH keys. | exp: func:Settings(), call:useMonitoringSettings, call:useSSHKeys, call:useSaveMonitoringMachine, call:useDeleteMonitoringMachine, call:useTestMonitoringMachineSSH, call:useState, call:emptyMachine, call:useMemo, call:orderedMachines.find, call:setSSHValidationMessage, call:setSSHValidationError, call:setSSHValidationStatus, call:clearSSHValidation, call:setMachineDraft, call:setEditingMachine, call:setMachineDialogOpen, call:saveMachine.mutateAsync, call:testMachineSSH.mutateAsync, call:String, call:message.toLowerCase, call:lowered.includes, call:setTab, call:orderedMachines.map, call:setSelectedMachineId, call:cn, call:openEditMachine, call:setDeleteMachineId, call:closeMachineDialog, call:saveMachineDraft, call:machineDraft.host.trim, call:Boolean, call:deleteMachine.mutate | dep: react, ../types, ../hooks/useSettings, ../components/DialogFooter, ../components/HoverEditButton, ../components/SectionCard, ../components/SelectionRailCard, ../components/TabbedCard, ../components/ConfirmDialog, @/lib/utils, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/card, @/components/ui/checkbox, @/components/ui/dialog, @/components/ui/input, @/components/ui/label, @/components/ui/select, @/components/ui/switch, @/components/ui/tabs, @/components/ui/textarea
|
||||
- Settings.tsx | Provides a React settings UI for managing monitoring machines, SSH keys, and local database operations with form editing and validation. | exp: func:Settings(), call:useMonitoringSettings, call:useSSHKeys, call:useSaveMonitoringMachine, call:useDeleteMonitoringMachine, call:useTestMonitoringMachineSSH, call:useState, call:emptyMachine, call:useMemo, call:orderedMachines.find, call:setSSHValidationMessage, call:setSSHValidationError, call:setSSHValidationStatus, call:clearSSHValidation, call:setMachineDraft, call:setEditingMachine, call:setMachineDialogOpen, call:saveMachine.mutateAsync, call:testMachineSSH.mutateAsync, call:String, call:message.toLowerCase, call:lowered.includes, call:setTab, call:orderedMachines.map, call:setSelectedMachineId, call:cn, call:openEditMachine, call:setDeleteMachineId, call:closeMachineDialog, call:saveMachineDraft, call:machineDraft.host.trim, call:Boolean, call:deleteMachine.mutate | dep: react, ../types, ../hooks/useSettings, ../components/DialogFooter, ../components/HoverEditButton, ../components/SectionCard, ../components/SelectionRailCard, ../components/TabbedCard, ../components/ConfirmDialog, @/lib/utils, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/card, @/components/ui/checkbox, @/components/ui/dialog, @/components/ui/input, @/components/ui/label, @/components/ui/select, @/components/ui/switch, @/components/ui/tabs, @/components/ui/textarea, @/components/ui/*
|
||||
- Users.tsx | Re-exports the UsersPage component from its implementation file to provide a cleaner import interface. | dep: ./UsersPage.impl
|
||||
- UsersPage.impl.tsx | A React page component for managing and messaging Jellyfin users with Jellyseerr enrichment, featuring a searchable directory table, user selection, email composition dialog, and queue status monitoring. | exp: func:UsersPage(), call:useUsers, call:useActivity, call:useUserMessageQueueStatus, call:useSendUserMessage, call:useIsMobile, call:useState, call:useSearchParams, call:useRef, call:useMemo, call:mergeUsersWithActivity, call:search.trim().toLowerCase, call:rows.filter, call:[ row.username, row.display_name, row.email, row.email_source, row.avatar_source, row.name_source, row.access_source, row.user_type_label, row.role, row.permissions_label, row.jellyseerr_username, row.activity_label, row.activity_summary, row.activity.primary_session?.title || "", String(row.jellyseerr_user_id ?? ""), ].some, call:String, call:value.toLowerCase().includes, call:queueStatus.active_request_id.slice, call:selectedIdSet.has, call:selectedRows.filter, call:filteredRows.filter, call:setSelectedUserIds, call:current.includes, call:current.filter, call:filteredRows.forEach, call:next.add, call:next.delete, call:Array.from, call:searchParams.get, call:resolveUserSelection, call:buildUserDrawerModel, call:sendUserMessage.reset, call:subject.trim, call:setSubject, call:htmlBody.trim, call:setHtmlBody, call:setComposeOpen, call:htmlBody.slice, call:requestAnimationFrame, call:textarea.focus, call:textarea.setSelectionRange, call:window.prompt, call:insertMarkup, call:setAttachments, call:formData.append, call:JSON.stringify, call:allSelectedRows.map, call:attachments.forEach, call:sendUserMessage.mutateAsync, call:setSearch, call:cn, call:toggleVisibleSelection, call:filteredRows.map, call:setSearchParams, call:event.stopPropagation, call:toggleUserSelected, call:userLabel(row).charAt(0).toUpperCase, call:activityBadgeVariant, call:Boolean, call:drawerModel.title.charAt(0).toUpperCase, call:drawerModel.identity.map, call:drawerModel.contactActions.map, call:drawerModel.contactActions .map((action) => action.hint) .join, call:drawerModel.permissions.map, call:closeCompose, call:sendUserMessage.data.request_id.slice, call:selectedDeliverableRows.map, call:attachments.map, call:removeAttachment | dep: react, react-router-dom, lucide-react, @/components/ui/dialog, @/components/ui/input, @/components/ui/textarea, @/components/ui/separator, @/components/ui/label, @/components/ui/avatar, @/components/ui/badge, @/components/ui/button, @/components/ui/checkbox, @/components/ui/alert, @/components/ui/progress, @/components/ui/tooltip, @/components/ui/sheet, @/components/ui/table, @/lib/utils, ../components/MetricCard, ../components/SessionActivityPanel, ../hooks/useUsers, ../hooks/useDashboard, ../hooks/useSendUserMessage, ../hooks/useUserMessageQueueStatus, ../types, ../users, ../userState, @/components/ui (dialog, input, textarea, separator, label, avatar, badge, button, checkbox, alert, progress, tooltip, sheet, table)
|
||||
## arch
|
||||
React functional component pattern with lazy-loaded implementations (`.impl.tsx`) behind clean re-export facades (e.g., `FileBrowser.tsx`, `Users.tsx`), using dynamic service-type schemas and tabbed dashboard composition.
|
||||
React functional component pattern with implementation/re-export splitting; each page encapsulates its own state management, CRUD operations, and UI rendering, typically integrating backend APIs, dynamic forms, and tabular/dialog-based interfaces.
|
||||
## tags
|
||||
call:use, components, ui, call:set, state, service, machine, react
|
||||
call:use, components, ui, call:set, state, service, react, machine
|
||||
## symbols
|
||||
- Actions
|
||||
- Applications
|
||||
|
||||
@@ -10,8 +10,13 @@ import {
|
||||
useDeleteServiceInstance,
|
||||
useSaveServiceInstance,
|
||||
useServiceInstances,
|
||||
useServiceTypes,
|
||||
} from "../hooks/useServices";
|
||||
import type { ServiceInstance, ServiceInstanceInput } from "../types";
|
||||
import type {
|
||||
ServiceInstance,
|
||||
ServiceInstanceInput,
|
||||
ServiceTypeInfo,
|
||||
} from "../types";
|
||||
import { SectionCard } from "../components/SectionCard";
|
||||
import { ConfirmDialog } from "../components/ConfirmDialog";
|
||||
import { getServiceBinding } from "../integrations/registry";
|
||||
@@ -44,6 +49,7 @@ export function ServicePage() {
|
||||
serviceId: string;
|
||||
}>();
|
||||
const { data: services = [] } = useServiceInstances(serviceType || undefined);
|
||||
const { data: types = [] } = useServiceTypes();
|
||||
const saveService = useSaveServiceInstance();
|
||||
const deleteService = useDeleteServiceInstance();
|
||||
|
||||
@@ -52,9 +58,14 @@ export function ServicePage() {
|
||||
[services, serviceId],
|
||||
);
|
||||
const binding = getServiceBinding(serviceType);
|
||||
const typeInfo = useMemo(
|
||||
() => types.find((t) => t.service_type === serviceType),
|
||||
[types, serviceType],
|
||||
);
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [draftConfig, setDraftConfig] = useState<Record<string, unknown>>({});
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
|
||||
@@ -62,6 +73,7 @@ export function ServicePage() {
|
||||
if (instance && !hydrated) {
|
||||
setName(instance.name);
|
||||
setEnabled(instance.enabled);
|
||||
setDraftConfig({ ...instance.config });
|
||||
setHydrated(true);
|
||||
}
|
||||
|
||||
@@ -86,7 +98,7 @@ export function ServicePage() {
|
||||
id: instance!.id,
|
||||
service_type: instance!.service_type,
|
||||
name,
|
||||
config: instance!.config,
|
||||
config: draftConfig,
|
||||
secrets: {}, // secrets are managed via the dedicated inputs below
|
||||
enabled,
|
||||
};
|
||||
@@ -134,7 +146,12 @@ export function ServicePage() {
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<ServiceSecretsCard instance={instance} />
|
||||
<ServiceConnectionCard
|
||||
instance={instance}
|
||||
typeInfo={typeInfo}
|
||||
draftConfig={draftConfig}
|
||||
onConfigChange={setDraftConfig}
|
||||
/>
|
||||
|
||||
{binding.widgets.length > 0 ? (
|
||||
<SectionCard
|
||||
@@ -178,28 +195,82 @@ export function ServicePage() {
|
||||
);
|
||||
}
|
||||
|
||||
function ServiceSecretsCard({ instance }: { instance: ServiceInstance }) {
|
||||
function ServiceConnectionCard({
|
||||
instance,
|
||||
typeInfo,
|
||||
draftConfig,
|
||||
onConfigChange,
|
||||
}: {
|
||||
instance: ServiceInstance;
|
||||
typeInfo: ServiceTypeInfo | undefined;
|
||||
draftConfig: Record<string, unknown>;
|
||||
onConfigChange: (config: Record<string, unknown>) => void;
|
||||
}) {
|
||||
const saveService = useSaveServiceInstance();
|
||||
// Empty-on-edit: local state starts blank; a blank field means "keep existing".
|
||||
const [draftSecrets, setDraftSecrets] = useState<Record<string, string>>({});
|
||||
|
||||
const properties =
|
||||
(
|
||||
(typeInfo?.config_schema ?? {}) as {
|
||||
properties?: Record<
|
||||
string,
|
||||
{ type?: string; description?: string; default?: unknown }
|
||||
>;
|
||||
}
|
||||
).properties ?? {};
|
||||
const configEntries: Array<
|
||||
[string, { type?: string; description?: string }]
|
||||
> =
|
||||
Object.keys(properties).length > 0
|
||||
? Object.entries(properties).map(([key, schema]) => [
|
||||
key,
|
||||
{ type: schema?.type, description: schema?.description },
|
||||
])
|
||||
: Object.entries(instance.config).map(([key, value]) => [
|
||||
key,
|
||||
{ type: typeof value === "number" ? "integer" : "string" },
|
||||
]);
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
title="Connection"
|
||||
description="Non-secret config is read-only here for now; edit secret values below."
|
||||
description="Edit non-secret connection config and secret values."
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
{Object.entries(instance.config).length === 0 ? (
|
||||
{configEntries.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No connection config.</p>
|
||||
) : (
|
||||
<dl className="grid grid-cols-1 gap-2 text-sm sm:grid-cols-2">
|
||||
{Object.entries(instance.config).map(([key, value]) => (
|
||||
<div key={key} className="flex flex-col">
|
||||
<dt className="text-xs text-muted-foreground">{key}</dt>
|
||||
<dd className="truncate font-mono text-xs">{String(value)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<div className="flex flex-col gap-3">
|
||||
{configEntries.map(([key, schema]) => {
|
||||
const isNumber =
|
||||
schema.type === "integer" || schema.type === "number";
|
||||
return (
|
||||
<Field
|
||||
key={key}
|
||||
label={key}
|
||||
htmlFor={`cfg-${key}`}
|
||||
helper={schema.description}
|
||||
>
|
||||
<Input
|
||||
id={`cfg-${key}`}
|
||||
type={isNumber ? "number" : "text"}
|
||||
value={String(draftConfig[key] ?? "")}
|
||||
onChange={(e) =>
|
||||
onConfigChange({
|
||||
...draftConfig,
|
||||
[key]: isNumber
|
||||
? e.target.value === ""
|
||||
? undefined
|
||||
: Number(e.target.value)
|
||||
: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(instance.secrets_set).length === 0 ? (
|
||||
@@ -229,26 +300,27 @@ function ServiceSecretsCard({ instance }: { instance: ServiceInstance }) {
|
||||
{isSet ? <Badge variant="secondary">set</Badge> : null}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
onClick={() => {
|
||||
const onlyChanged = Object.fromEntries(
|
||||
Object.entries(draftSecrets).filter(([, v]) => v !== ""),
|
||||
);
|
||||
saveService.mutate({
|
||||
id: instance.id,
|
||||
service_type: instance.service_type,
|
||||
name: instance.name,
|
||||
config: instance.config,
|
||||
secrets: onlyChanged,
|
||||
enabled: instance.enabled,
|
||||
});
|
||||
setDraftSecrets({});
|
||||
}}
|
||||
>
|
||||
Update secrets
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
const onlyChanged = Object.fromEntries(
|
||||
Object.entries(draftSecrets).filter(([, v]) => v !== ""),
|
||||
);
|
||||
saveService.mutate({
|
||||
id: instance.id,
|
||||
service_type: instance.service_type,
|
||||
name: instance.name,
|
||||
config: draftConfig,
|
||||
secrets: onlyChanged,
|
||||
enabled: instance.enabled,
|
||||
});
|
||||
setDraftSecrets({});
|
||||
}}
|
||||
>
|
||||
Update connection
|
||||
</Button>
|
||||
</div>
|
||||
</SectionCard>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user