mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Promote Machines web interface editor
This commit is contained in:
@@ -26,6 +26,7 @@ interface ResourceDetailDrawerProps {
|
||||
onClose?: () => void;
|
||||
presentation?: ResourceDetailDrawerPresentation;
|
||||
resolveResourceLabel?: (resourceId: string) => string | null | undefined;
|
||||
initialShowAccessContext?: boolean;
|
||||
initialShowTrueNASDetails?: boolean;
|
||||
}
|
||||
|
||||
@@ -55,6 +56,7 @@ const DrawerContent: Component<ResourceDetailDrawerProps> = (props) => {
|
||||
resource: props.resource,
|
||||
presentation: presentation(),
|
||||
resolveResourceLabel: props.resolveResourceLabel,
|
||||
initialShowAccessContext: props.initialShowAccessContext,
|
||||
initialShowTrueNASDetails: props.initialShowTrueNASDetails,
|
||||
});
|
||||
const headingId = () => `resource-detail-drawer-heading-${props.resource.id}`;
|
||||
@@ -337,6 +339,7 @@ export const ResourceDetailDrawer: Component<ResourceDetailDrawerProps> = (props
|
||||
onClose={props.onClose}
|
||||
presentation={props.presentation}
|
||||
resolveResourceLabel={props.resolveResourceLabel}
|
||||
initialShowAccessContext={props.initialShowAccessContext}
|
||||
initialShowTrueNASDetails={props.initialShowTrueNASDetails}
|
||||
/>
|
||||
);
|
||||
|
||||
+116
-107
@@ -181,6 +181,113 @@ const timelineSourceAdapterOptions: Array<{
|
||||
})),
|
||||
];
|
||||
|
||||
const AccessDisclosure: Component<{
|
||||
drawer: UseResourceDetailDrawerStateResult;
|
||||
class?: string;
|
||||
}> = (props) => (
|
||||
<SupportDisclosure
|
||||
title="Access"
|
||||
summary={props.drawer.accessSummary()}
|
||||
expanded={props.drawer.showAccessContext()}
|
||||
onToggle={() => props.drawer.setShowAccessContext((value) => !value)}
|
||||
showLabel="Show access"
|
||||
hideLabel="Hide access"
|
||||
class={props.class}
|
||||
contentClass="mt-3 space-y-3"
|
||||
dataTestId="resource-access-section"
|
||||
>
|
||||
<Show when={props.drawer.relatedLinks().length > 0}>
|
||||
<div class="space-y-1">
|
||||
<div class="text-[10px] font-medium uppercase tracking-wide text-base-content">Links</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<For each={props.drawer.relatedLinks()}>
|
||||
{(link) => (
|
||||
<a
|
||||
href={link.href}
|
||||
aria-label={link.ariaLabel}
|
||||
class="inline-flex items-center rounded border border-blue-200 bg-blue-50 px-2.5 py-1 text-[11px] font-medium text-blue-700 transition-colors hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-200 dark:hover:bg-blue-900"
|
||||
>
|
||||
{link.compactLabel}
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.drawer.discoveryConfig()}>
|
||||
{(config) => (
|
||||
<div class="space-y-3">
|
||||
<WebInterfaceUrlField
|
||||
metadataKind={config().metadataKind}
|
||||
metadataId={config().metadataId}
|
||||
targetLabel={config().targetLabel}
|
||||
title="Web interface"
|
||||
discoveryLoading={props.drawer.discoveryLoading()}
|
||||
suggestedUrl={props.drawer.discoveryIdentifiedSummary()?.suggestedUrl}
|
||||
suggestedUrlReasonText={
|
||||
props.drawer.discoveryIdentifiedSummary()?.suggestedUrlReasonText
|
||||
}
|
||||
suggestedUrlReasonTitle={
|
||||
props.drawer.discoveryIdentifiedSummary()?.suggestedUrlReasonTitle
|
||||
}
|
||||
suggestedUrlDiagnostic={
|
||||
props.drawer.discoveryIdentifiedSummary()?.suggestedUrlDiagnostic
|
||||
}
|
||||
embedded
|
||||
/>
|
||||
|
||||
<Show when={!props.drawer.hasDiscoveryTab()}>
|
||||
<div
|
||||
class="space-y-2 border-t border-border pt-3"
|
||||
data-testid="resource-access-analysis"
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-[10px] font-medium uppercase tracking-wide text-base-content">
|
||||
Analysis
|
||||
</div>
|
||||
<Show when={props.drawer.discoveryContextSummary()}>
|
||||
<div class="mt-1 text-[10px] text-base-content">
|
||||
{props.drawer.discoveryContextSummary()}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.drawer.setShowDiscoveryContext((value) => !value)}
|
||||
class="inline-flex items-center rounded-md border border-border bg-surface px-2.5 py-1 text-[10px] font-medium text-base-content transition-colors hover:bg-base"
|
||||
>
|
||||
{props.drawer.showDiscoveryContext() ? 'Hide analysis' : 'Open analysis'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={props.drawer.showDiscoveryContext()}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
||||
<span class="ml-2 text-sm text-muted">{getDiscoveryLoadingState().text}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DiscoveryTab
|
||||
resourceType={config().resourceType}
|
||||
agentId={config().agentId}
|
||||
resourceId={config().resourceId}
|
||||
hostname={config().hostname}
|
||||
commandsEnabled={props.drawer.agentMeta()?.commandsEnabled}
|
||||
/>
|
||||
</Suspense>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</SupportDisclosure>
|
||||
);
|
||||
|
||||
export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOverviewTabProps> = (
|
||||
props,
|
||||
) => {
|
||||
@@ -204,9 +311,15 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
|
||||
const shouldPromoteTrueNASDetails = () => compactTableRow() && drawer.hasTrueNASDetails();
|
||||
const shouldPromoteHostDetails = () =>
|
||||
compactTableRow() && isPulseAgentPlatformResource(resource) && drawer.hasHostDetails();
|
||||
const shouldPromoteAccessContext = () =>
|
||||
compactTableRow() && drawer.showAccessContext() && drawer.hasAccessContext();
|
||||
|
||||
return (
|
||||
<div class="space-y-3">
|
||||
<Show when={shouldPromoteAccessContext()}>
|
||||
<AccessDisclosure drawer={drawer} />
|
||||
</Show>
|
||||
|
||||
<Show when={shouldPromoteTrueNASDetails()}>
|
||||
<TrueNASDetailsDisclosure drawer={drawer} class="space-y-2" contentClass="space-y-2" />
|
||||
</Show>
|
||||
@@ -518,7 +631,7 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
|
||||
drawer.hasVMwareDetails() ||
|
||||
drawer.hasTrueNASDetails() ||
|
||||
(drawer.hasHostDetails() && !shouldPromoteHostDetails()) ||
|
||||
drawer.hasAccessContext() ||
|
||||
(drawer.hasAccessContext() && !shouldPromoteAccessContext()) ||
|
||||
drawer.hasInvestigationContext()
|
||||
}
|
||||
>
|
||||
@@ -530,112 +643,8 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
|
||||
<TrueNASDetailsDisclosure drawer={drawer} class="h-full" />
|
||||
</Show>
|
||||
|
||||
<Show when={drawer.hasAccessContext()}>
|
||||
<SupportDisclosure
|
||||
title="Access"
|
||||
summary={drawer.accessSummary()}
|
||||
expanded={drawer.showAccessContext()}
|
||||
onToggle={() => drawer.setShowAccessContext((value) => !value)}
|
||||
showLabel="Show access"
|
||||
hideLabel="Hide access"
|
||||
class="h-full"
|
||||
contentClass="mt-3 space-y-3"
|
||||
dataTestId="resource-access-section"
|
||||
>
|
||||
<Show when={drawer.relatedLinks().length > 0}>
|
||||
<div class="space-y-1">
|
||||
<div class="text-[10px] font-medium uppercase tracking-wide text-base-content">
|
||||
Links
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<For each={drawer.relatedLinks()}>
|
||||
{(link) => (
|
||||
<a
|
||||
href={link.href}
|
||||
aria-label={link.ariaLabel}
|
||||
class="inline-flex items-center rounded border border-blue-200 bg-blue-50 px-2.5 py-1 text-[11px] font-medium text-blue-700 transition-colors hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-200 dark:hover:bg-blue-900"
|
||||
>
|
||||
{link.compactLabel}
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={drawer.discoveryConfig()}>
|
||||
{(config) => (
|
||||
<div class="space-y-3">
|
||||
<WebInterfaceUrlField
|
||||
metadataKind={config().metadataKind}
|
||||
metadataId={config().metadataId}
|
||||
targetLabel={config().targetLabel}
|
||||
title="Web interface"
|
||||
discoveryLoading={drawer.discoveryLoading()}
|
||||
suggestedUrl={drawer.discoveryIdentifiedSummary()?.suggestedUrl}
|
||||
suggestedUrlReasonText={
|
||||
drawer.discoveryIdentifiedSummary()?.suggestedUrlReasonText
|
||||
}
|
||||
suggestedUrlReasonTitle={
|
||||
drawer.discoveryIdentifiedSummary()?.suggestedUrlReasonTitle
|
||||
}
|
||||
suggestedUrlDiagnostic={
|
||||
drawer.discoveryIdentifiedSummary()?.suggestedUrlDiagnostic
|
||||
}
|
||||
embedded
|
||||
/>
|
||||
|
||||
<Show when={!drawer.hasDiscoveryTab()}>
|
||||
<div
|
||||
class="space-y-2 border-t border-border pt-3"
|
||||
data-testid="resource-access-analysis"
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-[10px] font-medium uppercase tracking-wide text-base-content">
|
||||
Analysis
|
||||
</div>
|
||||
<Show when={drawer.discoveryContextSummary()}>
|
||||
<div class="mt-1 text-[10px] text-base-content">
|
||||
{drawer.discoveryContextSummary()}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => drawer.setShowDiscoveryContext((value) => !value)}
|
||||
class="inline-flex items-center rounded-md border border-border bg-surface px-2.5 py-1 text-[10px] font-medium text-base-content transition-colors hover:bg-base"
|
||||
>
|
||||
{drawer.showDiscoveryContext() ? 'Hide analysis' : 'Open analysis'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={drawer.showDiscoveryContext()}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" />
|
||||
<span class="ml-2 text-sm text-muted">
|
||||
{getDiscoveryLoadingState().text}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DiscoveryTab
|
||||
resourceType={config().resourceType}
|
||||
agentId={config().agentId}
|
||||
resourceId={config().resourceId}
|
||||
hostname={config().hostname}
|
||||
commandsEnabled={drawer.agentMeta()?.commandsEnabled}
|
||||
/>
|
||||
</Suspense>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</SupportDisclosure>
|
||||
<Show when={drawer.hasAccessContext() && !shouldPromoteAccessContext()}>
|
||||
<AccessDisclosure drawer={drawer} class="h-full" />
|
||||
</Show>
|
||||
|
||||
<Show when={drawer.hasInvestigationContext()}>
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface UseResourceDetailDrawerStateOptions {
|
||||
resource: Resource;
|
||||
resolveResourceLabel?: (resourceId: string) => string | null | undefined;
|
||||
presentation?: ResourceDetailDrawerPresentation;
|
||||
initialShowAccessContext?: boolean;
|
||||
initialShowTrueNASDetails?: boolean;
|
||||
}
|
||||
|
||||
@@ -36,7 +37,9 @@ export const useResourceDetailDrawerState = (options: UseResourceDetailDrawerSta
|
||||
const [copied, setCopied] = createSignal(false);
|
||||
const [showReportModal, setShowReportModal] = createSignal(false);
|
||||
const [showHistoryFilters, setShowHistoryFilters] = createSignal(false);
|
||||
const [showAccessContext, setShowAccessContext] = createSignal(false);
|
||||
const [showAccessContext, setShowAccessContext] = createSignal(
|
||||
options.initialShowAccessContext === true,
|
||||
);
|
||||
const [showInvestigationContext, setShowInvestigationContext] = createSignal(false);
|
||||
const [showDiscoveryContext, setShowDiscoveryContext] = createSignal(false);
|
||||
const [showHostDetails, setShowHostDetails] = createSignal(
|
||||
@@ -73,6 +76,12 @@ export const useResourceDetailDrawerState = (options: UseResourceDetailDrawerSta
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (options.initialShowAccessContext === true) {
|
||||
setShowAccessContext(true);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const current = activeTab();
|
||||
const available = new Set(derived.tabs().map((tab) => tab.id));
|
||||
|
||||
@@ -172,7 +172,7 @@ export const WebInterfaceUrlField: Component<WebInterfaceUrlFieldProps> = (props
|
||||
</Show>
|
||||
|
||||
<p class="mt-1.5 text-[10px] text-muted">
|
||||
Add a URL to quickly access this {state.targetLabel()}'s web interface from Workloads.
|
||||
Add a URL to quickly access this {state.targetLabel()}'s web interface from Pulse.
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('WebInterfaceUrlField', () => {
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
"Add a URL to quickly access this workload's web interface from Workloads.",
|
||||
"Add a URL to quickly access this workload's web interface from Pulse.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export type PlatformResourceDetailState = {
|
||||
expandedResourceId: () => string | null;
|
||||
isExpanded: (resource: ResourceLike) => boolean;
|
||||
detailRowId: (resource: ResourceLike) => string;
|
||||
open: (resource: ResourceLike) => void;
|
||||
toggle: (resource: ResourceLike) => void;
|
||||
close: (resource?: ResourceLike) => void;
|
||||
handleActivationKey: (
|
||||
@@ -31,6 +32,9 @@ export function createPlatformResourceDetailState(options: {
|
||||
|
||||
const isExpanded = (resource: ResourceLike): boolean => expandedResourceId() === resource.id;
|
||||
const detailRowId = (resource: ResourceLike): string => `${options.idPrefix}-${resource.id}`;
|
||||
const open = (resource: ResourceLike) => {
|
||||
setExpandedResourceId(resource.id);
|
||||
};
|
||||
const toggle = (resource: ResourceLike) => {
|
||||
setExpandedResourceId((current) => (current === resource.id ? null : resource.id));
|
||||
};
|
||||
@@ -51,6 +55,7 @@ export function createPlatformResourceDetailState(options: {
|
||||
expandedResourceId,
|
||||
isExpanded,
|
||||
detailRowId,
|
||||
open,
|
||||
toggle,
|
||||
close,
|
||||
handleActivationKey,
|
||||
@@ -70,6 +75,7 @@ export const PlatformResourceDetailTableRow: Component<{
|
||||
detailRowId: string;
|
||||
colSpan: number;
|
||||
resolveResourceLabel?: (resourceId: string) => string | null | undefined;
|
||||
initialShowAccessContext?: boolean;
|
||||
initialShowTrueNASDetails?: boolean;
|
||||
onClose?: () => void;
|
||||
}> = (props) => {
|
||||
@@ -92,6 +98,7 @@ export const PlatformResourceDetailTableRow: Component<{
|
||||
resource={props.resource}
|
||||
presentation="table-row"
|
||||
resolveResourceLabel={props.resolveResourceLabel}
|
||||
initialShowAccessContext={props.initialShowAccessContext}
|
||||
initialShowTrueNASDetails={initialShowTrueNASDetails()}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
|
||||
@@ -1008,6 +1008,7 @@ export const AgentsMachinesTable: Component<{
|
||||
);
|
||||
const drawer = createPlatformResourceDetailState({ idPrefix: 'agents-machine-drawer' });
|
||||
const [agentMetadataById, setAgentMetadataById] = createSignal<Record<string, AgentMetadata>>({});
|
||||
const [accessTargetResourceId, setAccessTargetResourceId] = createSignal<string | null>(null);
|
||||
const visibleColumns = createMemo(
|
||||
() => columnVisibility.visibleColumns() as AgentMachineColumn[],
|
||||
);
|
||||
@@ -1209,6 +1210,22 @@ export const AgentsMachinesTable: Component<{
|
||||
const detailRowId = () => drawer.detailRowId(machine);
|
||||
const agentMetadataId = () => agentMetadataIdFor(machine);
|
||||
const savedWebInterfaceUrl = () => savedAgentCustomUrlFor(agentMetadataId());
|
||||
const clearAccessTargetIfCurrent = () => {
|
||||
if (accessTargetResourceId() === machine.id) setAccessTargetResourceId(null);
|
||||
};
|
||||
const toggleDetails = () => {
|
||||
const wasExpanded = isExpanded();
|
||||
drawer.toggle(machine);
|
||||
if (wasExpanded) clearAccessTargetIfCurrent();
|
||||
};
|
||||
const handleDetailsActivationKey: JSX.EventHandler<
|
||||
HTMLTableRowElement,
|
||||
KeyboardEvent
|
||||
> = (event) => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return;
|
||||
event.preventDefault();
|
||||
toggleDetails();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1217,8 +1234,8 @@ export const AgentsMachinesTable: Component<{
|
||||
aria-controls={isExpanded() ? detailRowId() : undefined}
|
||||
aria-expanded={isExpanded() ? 'true' : 'false'}
|
||||
data-agents-machine-row={machine.id}
|
||||
onClick={() => drawer.toggle(machine)}
|
||||
onKeyDown={drawer.handleActivationKey(machine)}
|
||||
onClick={toggleDetails}
|
||||
onKeyDown={handleDetailsActivationKey}
|
||||
tabIndex={0}
|
||||
>
|
||||
<TableCell
|
||||
@@ -1393,7 +1410,8 @@ export const AgentsMachinesTable: Component<{
|
||||
name={name()}
|
||||
canConfigure={Boolean(agentMetadataId())}
|
||||
onConfigure={() => {
|
||||
if (!isExpanded()) drawer.toggle(machine);
|
||||
setAccessTargetResourceId(machine.id);
|
||||
drawer.open(machine);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
@@ -1440,7 +1458,11 @@ export const AgentsMachinesTable: Component<{
|
||||
open={isExpanded()}
|
||||
detailRowId={detailRowId()}
|
||||
colSpan={detailColspan()}
|
||||
onClose={() => drawer.close(machine)}
|
||||
initialShowAccessContext={accessTargetResourceId() === machine.id}
|
||||
onClose={() => {
|
||||
drawer.close(machine);
|
||||
clearAccessTargetIfCurrent();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,12 @@ import { RESOURCE_METADATA_CHANGED_EVENT } from '@/utils/resourceMetadataEvents'
|
||||
import { AgentsMachinesTable } from '../AgentsMachinesTable';
|
||||
|
||||
vi.mock('@/components/Infrastructure/ResourceDetailDrawer', () => ({
|
||||
ResourceDetailDrawer: () => <div data-testid="resource-detail-drawer" />,
|
||||
ResourceDetailDrawer: (props: { initialShowAccessContext?: boolean }) => (
|
||||
<div
|
||||
data-testid="resource-detail-drawer"
|
||||
data-initial-show-access-context={String(props.initialShowAccessContext ?? false)}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/agentMetadata', () => ({
|
||||
@@ -277,6 +282,10 @@ describe('AgentsMachinesTable', () => {
|
||||
screen.getByRole('button', { name: 'Add web interface URL for Event Host' }),
|
||||
);
|
||||
expect(screen.getByTestId('resource-detail-drawer')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('resource-detail-drawer')).toHaveAttribute(
|
||||
'data-initial-show-access-context',
|
||||
'true',
|
||||
);
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(RESOURCE_METADATA_CHANGED_EVENT, {
|
||||
|
||||
Reference in New Issue
Block a user