mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-23 16:03:30 +00:00
Stop redacting resource names in remaining operator-local UI
Continues the policy-redaction scope fix from abdde303a. The same wrong
invariant -- "operator-local UI redacts when policy says redact" -- was
also encoded in:
- alertResourceTableModel.getAlertResourceLabel (the /alerts table that
feeds AlertResourceTableMobile and AlertResourceTableRow)
- AlertResourceIncidentsPanel (the incident card shown after the operator
clicks into a resource)
- infrastructureSelectors.matchesSearch (/infrastructure search filter,
whose haystack already includes raw hostname/ips so redacting the
displayName was internally inconsistent)
- useResources filter().search (the shared resource hook's search
predicate, used by Settings panels, /storage, /alerts, FindingsPanel)
- recoveryRecordPresentation.getRecoveryLinkedResourceLabel (recovery
rollup labels rendered in /recovery)
- Settings/ResourcePicker (the operator's selection UI for the Reporting
Panel; the picker is local even when the report itself transmits)
All six surfaces are operator-local. Per docs/PRIVACY.md, resource-policy
redaction applies "Before non-local model requests leave the instance" --
it is a transmission-boundary policy, not a local-rendering policy. The
operator must see their own resource names to recognize, search, and
configure them.
Switch each callsite to getPreferredInfrastructureDisplayName. Invert the
five test fixtures (ResourceTable, ResourcePicker, infrastructureSelectors,
UnifiedResourceTable performance contract, WorkloadsSurface performance
contract) that locked in the old redaction-in-local-UI invariant.
Cloud-bound callers (AI Chat, organization sharing prepare-payload, the
resource-detail drawer's governance meta panel) still call
getPreferredResourceDisplayName and continue to redact -- their tests
are unchanged. The only call site of the governed helper that is left
not yet reclassified is problemResourcePresentation, which currently has
no production consumers; leaving it untouched.
This commit is contained in:
parent
7637f1efac
commit
a17f879a19
11 changed files with 51 additions and 36 deletions
|
|
@ -205,7 +205,10 @@ describe('ResourceTable', () => {
|
|||
expect(screen.getByText('my-vm-100')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses the governed label for policy-aware resources', () => {
|
||||
it('renders policy-redacted resources with their raw display name in operator-local UI', () => {
|
||||
// The alert resource table is local UI for the operator. Per docs/PRIVACY.md
|
||||
// resource-policy redaction is a transmission-boundary policy, not a
|
||||
// local-rendering policy.
|
||||
const props = makeProps({
|
||||
resources: [
|
||||
makeResource({
|
||||
|
|
@ -222,9 +225,9 @@ describe('ResourceTable', () => {
|
|||
});
|
||||
render(() => <ResourceTable {...props} />);
|
||||
|
||||
expect(screen.getByText('Production VM')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Edit thresholds for Production VM')).toBeInTheDocument();
|
||||
expect(screen.queryByText('secret-vm-1')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('secret-vm-1')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Edit thresholds for secret-vm-1')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Production VM')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders multiple resources', () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Resource as UnifiedResource, ResourcePolicy } from '@/types/resource';
|
||||
import { getPreferredResourceDisplayName } from '@/utils/resourceIdentity';
|
||||
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
|
||||
|
||||
const COLUMN_TOOLTIP_LOOKUP: Record<string, string> = {
|
||||
'cpu %': 'Percent CPU utilization allowed before an alert fires.',
|
||||
|
|
@ -249,7 +249,9 @@ export function alertResourceSupportsMetric(
|
|||
}
|
||||
|
||||
export function getAlertResourceLabel(resource: AlertResourceTableResourceLike): string {
|
||||
return getPreferredResourceDisplayName(resource as unknown as UnifiedResource);
|
||||
// Operator-local /alerts table; redaction is a transmission-boundary policy
|
||||
// (docs/PRIVACY.md). Use the raw infra display name to match /infrastructure.
|
||||
return getPreferredInfrastructureDisplayName(resource as unknown as UnifiedResource);
|
||||
}
|
||||
|
||||
function parseAlertMetricNumber(value: unknown): number | undefined {
|
||||
|
|
|
|||
|
|
@ -711,7 +711,8 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
expect(filtered[0]?.platformData?.sources).toEqual(['proxmox']);
|
||||
});
|
||||
|
||||
it('keeps governed resource search aligned with the infrastructure display label', () => {
|
||||
it('searches policy-redacted resources by their raw infrastructure display name', () => {
|
||||
// Local search; redaction is a transmission-boundary policy (docs/PRIVACY.md).
|
||||
const governedResource = makeResource(9, {
|
||||
name: 'secret-host-9',
|
||||
displayName: 'secret-host-9',
|
||||
|
|
@ -722,8 +723,8 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
aiSafeSummary: 'Production Host',
|
||||
});
|
||||
|
||||
expect(matchesSearch(governedResource, 'Production')).toBe(true);
|
||||
expect(matchesSearch(governedResource, 'secret-host-9')).toBe(false);
|
||||
expect(matchesSearch(governedResource, 'secret-host-9')).toBe(true);
|
||||
expect(matchesSearch(governedResource, 'Production')).toBe(false);
|
||||
});
|
||||
|
||||
it('suppresses non-blocking policy posture in host-table rows while preserving blocking policy badges', async () => {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,10 @@ describe('infrastructureSelectors', () => {
|
|||
expect(matchesSearch(resource, 'NODE-123')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches governed resources by the safe display label instead of the redacted hostname', () => {
|
||||
it('matches policy-redacted resources by their raw display name in operator-local search', () => {
|
||||
// The /infrastructure search box is local UI; redaction is a transmission
|
||||
// policy (docs/PRIVACY.md). Plus the haystack already includes raw
|
||||
// hostname/ips, so the displayName must be raw too for consistency.
|
||||
const governedResource = makeResource(2, {
|
||||
name: 'secret-node-2',
|
||||
displayName: 'secret-node-2',
|
||||
|
|
@ -72,8 +75,8 @@ describe('infrastructureSelectors', () => {
|
|||
aiSafeSummary: 'Production Node',
|
||||
});
|
||||
|
||||
expect(matchesSearch(governedResource, 'Production')).toBe(true);
|
||||
expect(matchesSearch(governedResource, 'secret-node-2')).toBe(false);
|
||||
expect(matchesSearch(governedResource, 'secret-node-2')).toBe(true);
|
||||
expect(matchesSearch(governedResource, 'Production')).toBe(false);
|
||||
});
|
||||
|
||||
it('matches by ip and tag and returns false when missing', () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import type { Resource } from '@/types/resource';
|
||||
import { getCpuPercent, getDiskPercent, getMemoryPercent } from '@/types/resource';
|
||||
import {
|
||||
getPreferredInfrastructureDisplayName,
|
||||
getPreferredResourceDisplayName,
|
||||
} from '@/utils/resourceIdentity';
|
||||
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
|
||||
import { getInfrastructureSystemIdentitySortLabel } from '@/utils/resourceBadgePresentation';
|
||||
import { normalizeSourcePlatformKey, type KnownSourcePlatform } from '@/utils/sourcePlatforms';
|
||||
import { getCanonicalStatusLabel, STATUS_SORT_ORDER } from '@/utils/status';
|
||||
|
|
@ -140,7 +137,9 @@ export const matchesSearch = (resource: Resource, term: string): boolean => {
|
|||
if (!term) return true;
|
||||
const normalizedTerm = term.toLowerCase();
|
||||
const candidates: string[] = [
|
||||
getPreferredResourceDisplayName(resource),
|
||||
// Local search box; the operator types raw names. Haystack already includes
|
||||
// raw hostname and ips, so the displayName must be raw too for consistency.
|
||||
getPreferredInfrastructureDisplayName(resource),
|
||||
resource.id,
|
||||
resource.identity?.hostname ?? '',
|
||||
...(resource.identity?.ips ?? []),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { SearchField } from '@/components/shared/SearchField';
|
|||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { useResources } from '@/hooks/useResources';
|
||||
import type { Resource, ResourceType } from '@/types/resource';
|
||||
import { getPreferredResourceDisplayName } from '@/utils/resourceIdentity';
|
||||
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
|
||||
import {
|
||||
getResourcePickerEmptyState,
|
||||
getResourcePickerTypeFilterLabel,
|
||||
|
|
@ -58,7 +58,7 @@ export function ResourcePicker(props: ResourcePickerProps) {
|
|||
const searchTerm = search().toLowerCase().trim();
|
||||
if (searchTerm) {
|
||||
result = result.filter((r) => {
|
||||
const name = getPreferredResourceDisplayName(r).toLowerCase();
|
||||
const name = getPreferredInfrastructureDisplayName(r).toLowerCase();
|
||||
const id = r.id.toLowerCase();
|
||||
return name.includes(searchTerm) || id.includes(searchTerm);
|
||||
});
|
||||
|
|
@ -75,8 +75,8 @@ export function ResourcePicker(props: ResourcePickerProps) {
|
|||
const aOrder = reportableResourceTypeSortOrder(a.type);
|
||||
const bOrder = reportableResourceTypeSortOrder(b.type);
|
||||
if (aOrder !== bOrder) return aOrder - bOrder;
|
||||
return getPreferredResourceDisplayName(a).localeCompare(
|
||||
getPreferredResourceDisplayName(b),
|
||||
return getPreferredInfrastructureDisplayName(a).localeCompare(
|
||||
getPreferredInfrastructureDisplayName(b),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ export function ResourcePicker(props: ResourcePickerProps) {
|
|||
{
|
||||
id: resource.id,
|
||||
type: resource.type,
|
||||
name: getPreferredResourceDisplayName(resource),
|
||||
name: getPreferredInfrastructureDisplayName(resource),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
|
@ -115,7 +115,7 @@ export function ResourcePicker(props: ResourcePickerProps) {
|
|||
.map((r) => ({
|
||||
id: r.id,
|
||||
type: r.type,
|
||||
name: getPreferredResourceDisplayName(r),
|
||||
name: getPreferredInfrastructureDisplayName(r),
|
||||
}));
|
||||
|
||||
const newSelection = [...current, ...toAdd];
|
||||
|
|
@ -253,7 +253,7 @@ export function ResourcePicker(props: ResourcePickerProps) {
|
|||
{/* Name and ID */}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm text-white sm:truncate break-words">
|
||||
{getPreferredResourceDisplayName(resource)}
|
||||
{getPreferredInfrastructureDisplayName(resource)}
|
||||
</div>
|
||||
<div class="text-xs text-slate-500 sm:truncate break-all">
|
||||
{resource.id}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,11 @@ describe('ResourcePicker', () => {
|
|||
expect(screen.getByText('TrueNAS')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders governed resources with the safe display label', async () => {
|
||||
it('renders policy-redacted resources with their raw display name (operator-local picker does not redact)', async () => {
|
||||
// The Reporting Panel picker is the operator selecting their own resources.
|
||||
// Per docs/PRIVACY.md, redaction is a transmission-boundary policy and must
|
||||
// not be applied to local /settings UI; the operator needs the raw name to
|
||||
// recognize the resource.
|
||||
mockResources = [
|
||||
makeResource({
|
||||
id: 'vm-2',
|
||||
|
|
@ -118,8 +122,8 @@ describe('ResourcePicker', () => {
|
|||
|
||||
renderPicker();
|
||||
|
||||
expect(await screen.findByText('Production VM')).toBeInTheDocument();
|
||||
expect(screen.queryByText('secret-vm-2')).not.toBeInTheDocument();
|
||||
expect(await screen.findByText('secret-vm-2')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Production VM')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies type filter buttons', async () => {
|
||||
|
|
|
|||
|
|
@ -428,11 +428,13 @@ describe('Workloads performance contract', () => {
|
|||
expect(mockUnifiedResourcesQuery).toContain('type=agent');
|
||||
});
|
||||
|
||||
it('keeps governed resource search aligned with the preferred display label', () => {
|
||||
it('searches policy-redacted resources by their raw display name in operator-local UI', () => {
|
||||
// /workloads search; redaction is a transmission-boundary policy
|
||||
// (docs/PRIVACY.md), so the haystack must use the raw infra name.
|
||||
const resources = [makeResource()];
|
||||
|
||||
const filtered = filterResources(resources, new Set(), new Set(), ['Production']);
|
||||
const rawFiltered = filterResources(resources, new Set(), new Set(), ['secret-host']);
|
||||
const filtered = filterResources(resources, new Set(), new Set(), ['secret-host']);
|
||||
const rawFiltered = filterResources(resources, new Set(), new Set(), ['Production']);
|
||||
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(rawFiltered).toHaveLength(0);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
type ResourceSurfaceLink,
|
||||
} from '@/routing/resourceLinks';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { getPreferredResourceDisplayName } from '@/utils/resourceIdentity';
|
||||
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
|
||||
import {
|
||||
getAlertIncidentLevelBadgeClass,
|
||||
getAlertIncidentStatusPresentation,
|
||||
|
|
@ -52,7 +52,7 @@ export function AlertResourceIncidentsPanel(props: AlertResourceIncidentsPanelPr
|
|||
const resourceDisplayName = () => {
|
||||
const current = resource();
|
||||
if (current) {
|
||||
return getPreferredResourceDisplayName(current);
|
||||
return getPreferredInfrastructureDisplayName(current);
|
||||
}
|
||||
return selection().resourceName;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import {
|
|||
getMemoryPercent,
|
||||
getDiskPercent,
|
||||
} from '@/types/resource';
|
||||
import { getPreferredResourceDisplayName } from '@/utils/resourceIdentity';
|
||||
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
|
||||
type ResourceStoreLike = Pick<ReturnType<typeof getGlobalWebSocketStore>, 'state'>;
|
||||
|
||||
export interface UseResourcesReturn {
|
||||
|
|
@ -204,7 +204,8 @@ export function useResources(storeOverride?: ResourceStoreLike): UseResourcesRet
|
|||
if (filter.search && filter.search.trim()) {
|
||||
const term = filter.search.toLowerCase().trim();
|
||||
result = result.filter((r) => {
|
||||
const name = getPreferredResourceDisplayName(r).toLowerCase();
|
||||
// Match the raw operator-facing name; redaction is a transmission policy.
|
||||
const name = getPreferredInfrastructureDisplayName(r).toLowerCase();
|
||||
const id = r.id.toLowerCase();
|
||||
return name.includes(term) || id.includes(term);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { ProtectionRollup, RecoveryPoint } from '@/types/recovery';
|
|||
import type { Resource } from '@/types/resource';
|
||||
import { getRecoveryArtifactModePresentation } from '@/utils/recoveryArtifactModePresentation';
|
||||
import { normalizeRecoveryOutcome } from '@/utils/recoveryOutcomePresentation';
|
||||
import { getPreferredResourceDisplayName } from '@/utils/resourceIdentity';
|
||||
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
|
||||
import { titleCaseDelimitedLabel } from '@/utils/textPresentation';
|
||||
|
||||
export type RecoveryArtifactMode = 'snapshot' | 'local' | 'remote';
|
||||
|
|
@ -14,7 +14,7 @@ const getRecoveryLinkedResourceLabel = (
|
|||
if (!itemResourceId) return '';
|
||||
const resource = resourcesById.get(itemResourceId);
|
||||
if (!resource) return '';
|
||||
const label = getPreferredResourceDisplayName(resource).trim();
|
||||
const label = getPreferredInfrastructureDisplayName(resource).trim();
|
||||
if (!label) return '';
|
||||
if (label.toLowerCase() === itemResourceId.toLowerCase()) return '';
|
||||
return label;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue