All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 2s
The PolicyDetailSheet component now properly handles both type unions: - PolicySettingRow (has graphPolicyId) - used in V2 - PolicySettingSearchResult (has id) - used in V1 Uses type guard to check for 'graphPolicyId' property and falls back to 'id' field. This fixes the TypeScript compilation error in production builds.
306 lines
9.8 KiB
TypeScript
306 lines
9.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from '@/components/ui/sheet';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import type { PolicySettingSearchResult } from '@/lib/actions/policySettings';
|
|
import type { PolicySettingRow } from '@/lib/types/policy-table';
|
|
import { PolicyTypeBadge } from './PolicyTypeBadge';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
import { de } from 'date-fns/locale';
|
|
import { Copy, ExternalLink, Check } from 'lucide-react';
|
|
import { useCopyToClipboard } from '@/lib/hooks/useCopyToClipboard';
|
|
import { getIntunePortalLink } from '@/lib/utils/policy-table-helpers';
|
|
|
|
interface PolicyDetailSheetProps {
|
|
policy: PolicySettingSearchResult | PolicySettingRow | null;
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
}
|
|
|
|
function isJsonString(str: string): boolean {
|
|
if (!str || typeof str !== 'string') return false;
|
|
const trimmed = str.trim();
|
|
return trimmed.startsWith('{') || trimmed.startsWith('[');
|
|
}
|
|
|
|
function formatJson(value: string): string {
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
return JSON.stringify(parsed, null, 2);
|
|
} catch {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
export function PolicyDetailSheet({
|
|
policy,
|
|
open,
|
|
onOpenChange,
|
|
}: PolicyDetailSheetProps) {
|
|
const [activeTab, setActiveTab] = useState<'details' | 'raw'>('details');
|
|
const { copy, isCopied } = useCopyToClipboard();
|
|
|
|
if (!policy) return null;
|
|
|
|
const isJson = isJsonString(policy.settingValue);
|
|
const displayValue = isJson
|
|
? formatJson(policy.settingValue)
|
|
: policy.settingValue;
|
|
|
|
// Handle both PolicySettingRow (has graphPolicyId) and PolicySettingSearchResult (has id)
|
|
const policyId = 'graphPolicyId' in policy ? policy.graphPolicyId : policy.id;
|
|
const intuneUrl = getIntunePortalLink(policy.policyType, policyId);
|
|
|
|
const handleCopyField = (value: string, fieldName: string) => {
|
|
copy(value, `${fieldName} copied to clipboard`);
|
|
};
|
|
|
|
const handleOpenInIntune = () => {
|
|
if (intuneUrl) {
|
|
window.open(intuneUrl, '_blank', 'noopener,noreferrer');
|
|
} else {
|
|
// Fallback: copy policy ID
|
|
copy(policyId, 'Policy ID copied to clipboard');
|
|
}
|
|
};
|
|
|
|
// Generate raw JSON for the entire policy object
|
|
const rawJson = JSON.stringify(policy, null, 2);
|
|
|
|
return (
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
<SheetContent className="w-[600px] sm:max-w-[600px] overflow-y-auto">
|
|
<SheetHeader>
|
|
<SheetTitle className="flex items-center justify-between">
|
|
<span className="truncate">{policy.settingName}</span>
|
|
{intuneUrl && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleOpenInIntune}
|
|
className="ml-2"
|
|
>
|
|
<ExternalLink className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</SheetTitle>
|
|
<SheetDescription>
|
|
Policy Setting Details
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
{/* Tabs */}
|
|
<div className="flex gap-2 mt-4 border-b">
|
|
<button
|
|
onClick={() => setActiveTab('details')}
|
|
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
activeTab === 'details'
|
|
? 'border-primary text-primary'
|
|
: 'border-transparent text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
Details
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('raw')}
|
|
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
activeTab === 'raw'
|
|
? 'border-primary text-primary'
|
|
: 'border-transparent text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
Raw JSON
|
|
</button>
|
|
</div>
|
|
|
|
{/* Details Tab */}
|
|
{activeTab === 'details' && (
|
|
<div className="mt-6 space-y-6">
|
|
{/* Policy Name */}
|
|
<div>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<h3 className="text-sm font-medium text-muted-foreground">
|
|
Policy Name
|
|
</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleCopyField(policy.policyName, 'Policy Name')}
|
|
className="h-7 px-2"
|
|
>
|
|
{isCopied ? (
|
|
<Check className="h-3 w-3 text-green-600" />
|
|
) : (
|
|
<Copy className="h-3 w-3" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
<p className="text-sm">{policy.policyName}</p>
|
|
</div>
|
|
|
|
{/* Policy Type */}
|
|
<div>
|
|
<h3 className="text-sm font-medium text-muted-foreground mb-1">
|
|
Policy Type
|
|
</h3>
|
|
<PolicyTypeBadge type={policy.policyType} />
|
|
</div>
|
|
|
|
{/* Setting Name */}
|
|
<div>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<h3 className="text-sm font-medium text-muted-foreground">
|
|
Setting Name
|
|
</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleCopyField(policy.settingName, 'Setting Name')}
|
|
className="h-7 px-2"
|
|
>
|
|
{isCopied ? (
|
|
<Check className="h-3 w-3 text-green-600" />
|
|
) : (
|
|
<Copy className="h-3 w-3" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
<p className="text-sm font-mono">{policy.settingName}</p>
|
|
</div>
|
|
|
|
{/* Setting Value */}
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h3 className="text-sm font-medium text-muted-foreground">
|
|
Setting Value
|
|
</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleCopyField(policy.settingValue, 'Setting Value')}
|
|
className="h-7 px-2"
|
|
>
|
|
{isCopied ? (
|
|
<Check className="h-3 w-3 text-green-600" />
|
|
) : (
|
|
<Copy className="h-3 w-3" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
{isJson ? (
|
|
<pre className="text-xs bg-muted p-4 rounded-md overflow-x-auto max-h-96 overflow-y-auto">
|
|
<code>{displayValue}</code>
|
|
</pre>
|
|
) : (
|
|
<p className="text-sm whitespace-pre-wrap break-words">
|
|
{displayValue}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Graph Policy ID */}
|
|
<div>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<h3 className="text-sm font-medium text-muted-foreground">
|
|
Graph Policy ID
|
|
</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleCopyField(policyId, 'Policy ID')}
|
|
className="h-7 px-2"
|
|
>
|
|
{isCopied ? (
|
|
<Check className="h-3 w-3 text-green-600" />
|
|
) : (
|
|
<Copy className="h-3 w-3" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
<p className="text-sm font-mono text-muted-foreground">
|
|
{policyId}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Last Synced */}
|
|
<div>
|
|
<h3 className="text-sm font-medium text-muted-foreground mb-1">
|
|
Last Synced
|
|
</h3>
|
|
<p className="text-sm">
|
|
{formatDistanceToNow(new Date(policy.lastSyncedAt), {
|
|
addSuffix: true,
|
|
locale: de,
|
|
})}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{new Date(policy.lastSyncedAt).toLocaleString('de-DE')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Open in Intune Button */}
|
|
<div className="pt-4 border-t">
|
|
{intuneUrl ? (
|
|
<Button
|
|
variant="default"
|
|
onClick={handleOpenInIntune}
|
|
className="w-full"
|
|
>
|
|
<ExternalLink className="mr-2 h-4 w-4" />
|
|
Open in Intune Portal
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => handleCopyField(policyId, 'Policy ID')}
|
|
className="w-full"
|
|
>
|
|
<Copy className="mr-2 h-4 w-4" />
|
|
Copy Policy ID
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Raw JSON Tab */}
|
|
{activeTab === 'raw' && (
|
|
<div className="mt-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-sm font-medium">Complete Policy Object</h3>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => copy(rawJson, 'Raw JSON copied to clipboard')}
|
|
>
|
|
{isCopied ? (
|
|
<>
|
|
<Check className="mr-2 h-4 w-4 text-green-600" />
|
|
Copied
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy className="mr-2 h-4 w-4" />
|
|
Copy All
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
<pre className="text-xs bg-muted p-4 rounded-md overflow-x-auto max-h-[600px] overflow-y-auto">
|
|
<code className="language-json">{rawJson}</code>
|
|
</pre>
|
|
</div>
|
|
)}
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
}
|