All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 2s
Implemented Tasks T038-T046: - T038: Created useCopyToClipboard hook with toast notifications - T039: Skipped (unit tests - optional) - T040: Added copy button for Policy ID field - T041: Added copy button for Setting Name field - T042: Added tabs for Details and Raw JSON views - T043: Implemented Raw JSON tab with syntax highlighting - T044: Created getIntunePortalLink utility (8 policy types) - T045: Added Open in Intune button with URL construction - T046: Fallback to copy Policy ID if URL unavailable Files Created: - lib/hooks/useCopyToClipboard.ts (65 lines) - lib/utils/policy-table-helpers.ts (127 lines) Files Updated: - components/policy-explorer/PolicyDetailSheet.tsx (enhanced with tabs, copy buttons, Intune links) Features: - Copy-to-clipboard for all fields with visual feedback - Two-tab interface: Details (enhanced fields) and Raw JSON (full object) - Deep linking to Intune portal by policy type - Clipboard API with document.execCommand fallback - Toast notifications for user feedback
303 lines
9.6 KiB
TypeScript
303 lines
9.6 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 { 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 | 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;
|
|
|
|
const intuneUrl = getIntunePortalLink(policy.policyType, policy.graphPolicyId);
|
|
|
|
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(policy.graphPolicyId, '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(policy.graphPolicyId, '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">
|
|
{policy.graphPolicyId}
|
|
</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(policy.graphPolicyId, '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>
|
|
);
|
|
}
|