added some info hover cards.

• found this this is probably the wrong direction
• will create a new page for next iteration.
This commit is contained in:
Jonathan Summers-Muir committed 2024-10-03 14:10:36 +08:00
1 parent 630455f2d4
commit 9591d8e04d
4 files changed
+423 -115

No files matched your search

@@ -0,0 +1,22 @@
import React from 'react'
import { GlobeLock } from 'lucide-react'
import { algorithmDetails, AlgorithmDetail } from './algorithmDetails'
import { InfoPill } from '../../ui/InfoPill'
interface AlgorithmHoverCardProps {
algorithm: keyof typeof algorithmDetails
}
export const AlgorithmHoverCard: React.FC<AlgorithmHoverCardProps> = ({ algorithm }) => {
const details: AlgorithmDetail = algorithmDetails[algorithm]
return (
<InfoPill
label={details.label}
icon={<GlobeLock className="w-4" strokeWidth={1} />}
title={details.name}
description={details.description}
links={details.links}
/>
)
}
@@ -29,6 +29,8 @@ import {
DialogTrigger,
DialogFooter,
DialogDescription,
DialogSection,
DialogSectionSeparator,
} from 'ui/src/components/shadcn/ui/dialog'
import {
Select,
@@ -54,10 +56,20 @@ import {
Eye,
ArrowUpCircle,
Key,
Fingerprint,
GlobeLock,
ExternalLink,
} from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
import { toast } from 'sonner'
import { cn } from 'ui'
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from 'ui/src/components/shadcn/ui/hover-card'
import { algorithmDetails } from './algorithmDetails'
import { AlgorithmHoverCard } from './AlgorithmHoverCard'
type KeyStatus = 'STANDBY' | 'IN_USE' | 'PREVIOUSLY_USED'
type JWTAlgorithm = 'HS256' | 'RS256' | 'ES256'
@@ -80,21 +92,12 @@ const statusLabels: Record<KeyStatus, string> = {
}
const statusColors: Record<KeyStatus, string> = {
STANDBY: 'text-xs font-mono uppercase bg-warning bg-opacity-100 text-warning-200 border-warning',
IN_USE: 'text-xs font-mono uppercase bg-brand bg-opacity-100 text-brand-200 border-brand',
PREVIOUSLY_USED: 'text-xs font-mono uppercase bg-alternative text-gray-800',
}
const algorithmLabels: Record<JWTAlgorithm, string> = {
HS256: 'HS256 (Symmetric)',
RS256: 'RS256 (RSA)',
ES256: 'ES256 (ECC)',
}
const algorithmDescriptions: Record<JWTAlgorithm, string> = {
HS256: 'HMAC with SHA-256: Fast, simple, requires secure key exchange',
RS256: 'RSA with SHA-256: Allows public key distribution, slower',
ES256: 'ECDSA with SHA-256: Compact keys, fast, modern alternative to RSA',
STANDBY:
'text-xs leading-normal px-1.5 font-mono py-0 uppercase bg-warning bg-opacity-100 text-warning-200 border-warning',
IN_USE:
'text-xs leading-normal px-1.5 font-mono py-0 uppercase bg-brand bg-opacity-100 text-brand-200 border-brand',
PREVIOUSLY_USED:
'text-xs leading-normal px-1.5 font-mono py-0 uppercase bg-alternative text-gray-800',
}
const initialKeys: SecretKey[] = [
@@ -337,6 +340,138 @@ const ApiKeySection: React.FC<{ inUseKey: SecretKey | undefined }> = ({ inUseKey
)
}
const StandbyKeyIllustration = () => (
<svg
width="179"
height="106"
viewBox="0 0 179 106"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="12.7177"
y="56.8446"
width="153.815"
height="22.5914"
rx="2.39438"
fill="#292929"
stroke="#2E2E2E"
stroke-width="0.435342"
/>
<rect
x="20.3456"
y="62.8678"
width="32.4201"
height="10.545"
rx="5.27252"
fill="#DB8E00"
stroke="#2E2E2E"
stroke-width="0.435342"
/>
<rect x="62.5447" y="66.8903" width="30.3622" height="2.5" rx="1.25" fill="#4D4D4D" />
<rect x="100.562" y="66.8903" width="42.6666" height="2.5" rx="1.25" fill="#4D4D4D" />
<rect
x="12.7177"
y="30.4939"
width="153.815"
height="22.5914"
rx="2.39438"
fill="#292929"
stroke="#2E2E2E"
stroke-width="0.435342"
/>
<rect
x="20.3456"
y="36.5171"
width="32.4201"
height="10.545"
rx="5.27252"
fill="#DB8E00"
stroke="#2E2E2E"
stroke-width="0.435342"
/>
<rect x="62.5447" y="40.5396" width="30.3622" height="2.5" rx="1.25" fill="#4D4D4D" />
<rect x="100.562" y="40.5396" width="42.6666" height="2.5" rx="1.25" fill="#4D4D4D" />
<rect
x="12.7177"
y="83.0563"
width="153.815"
height="22.5914"
rx="2.39438"
fill="#292929"
stroke="#2E2E2E"
stroke-width="0.435342"
/>
<rect
x="20.3456"
y="89.0795"
width="32.4201"
height="10.545"
rx="5.27252"
fill="#DB8E00"
stroke="#2E2E2E"
stroke-width="0.435342"
/>
<rect x="62.5447" y="93.1019" width="30.3622" height="2.5" rx="1.25" fill="#4D4D4D" />
<rect x="100.562" y="93.1019" width="42.6666" height="2.5" rx="1.25" fill="#4D4D4D" />
<rect
x="9.3874"
y="23.4523"
width="159.838"
height="25.2538"
rx="3.02066"
fill="#262626"
fill-opacity="0.75"
stroke="#2E2E2E"
stroke-width="0.549211"
/>
<rect
x="6.03046"
y="17.6493"
width="165.945"
height="25.2538"
rx="3.02066"
fill="#262626"
fill-opacity="0.75"
stroke="#2E2E2E"
stroke-width="0.549211"
/>
<rect
x="3.02461"
y="10.0497"
width="172.201"
height="27.0504"
rx="3.02066"
fill="#262626"
fill-opacity="0.75"
stroke="#2E2E2E"
stroke-width="0.549211"
/>
<rect
x="0.591012"
y="1.0001"
width="177.324"
height="28.5004"
rx="3.02066"
fill="#292929"
stroke="#4D4D4D"
stroke-width="0.549211"
/>
<rect
x="9.3874"
y="8.59867"
width="32.338"
height="13.3032"
rx="6.65161"
fill="#3ECF8E"
stroke="#2E2E2E"
stroke-width="0.549211"
/>
<rect x="49" y="13.266" width="33.5858" height="3.96857" rx="1.98428" fill="#4D4D4D" />
<rect x="91.0535" y="13.266" width="47.1966" height="3.96857" rx="1.98428" fill="#4D4D4D" />
</svg>
)
export function JWTSecretKeysTable() {
const [secretKeys, setSecretKeys] = useAtom(secretKeysAtom)
const [selectedKey, setSelectedKey] = useState<SecretKey | null>(null)
@@ -500,29 +635,21 @@ NEW_STANDBY_KEY_CONTENT
setReplacementKeyId(null)
}
const MotionTableRow = motion(TableRow)
const renderKeyRow = (key: SecretKey) => (
<MotionTableRow
<TableRow
key={key.id}
layout
initial={{ opacity: 0, height: 0 }}
animate={{
opacity: 1,
height: 'auto',
transition: { duration: 0.3 },
}}
exit={{ opacity: 0, height: 0 }}
className={`border-t border-dashed ${key.status === 'IN_USE' ? 'bg-surface-200' : ''} [&_td]:py-2`}
className={key.status !== 'IN_USE' ? 'border-b border-dashed border-border' : ''}
>
<TableCell className="">
<Badge className={`${statusColors[key.status]} font-medium px-2 py-1 rounded-full`}>
{statusLabels[key.status]}
<Badge className={`${statusColors[key.status]}`}>
<span>{statusLabels[key.status]}</span>
</Badge>
</TableCell>
<TableCell className="font-mono truncate max-w-[100px]">{key.keyId}</TableCell>
<TableCell className="truncate max-w-[150px]">{key.createdAt}</TableCell>
<TableCell className="truncate max-w-[100px]">{algorithmLabels[key.algorithm]}</TableCell>
<TableCell className="truncate max-w-[100px]">
<AlgorithmHoverCard algorithm={key.algorithm} />
</TableCell>
<TableCell className="text-right">
<Dialog>
<DropdownMenu>
@@ -589,13 +716,13 @@ NEW_STANDBY_KEY_CONTENT
</DialogContent>
</Dialog>
</TableCell>
</MotionTableRow>
</TableRow>
)
return (
<>
<div className="space-y-8">
<div className="flex justify-end">
{/* <div className="flex justify-end">
<Button
onClick={() => setShowCreateKeyDialog(true)}
disabled={actionInProgress !== null}
@@ -603,36 +730,53 @@ NEW_STANDBY_KEY_CONTENT
>
Create New Standby Key
</Button>
</div>
</div> */}
<div>
<Card className="w-full bg-surface-100 overflow-hidden">
<CardContent className="p-0">
<Table className="p-5">
<TableHeader>
<TableRow className="bg-surface-300">
<TableHead className="pl-5 text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
Status
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
Key ID
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
Created At
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
Type
</TableHead>
<TableHead className="pr-5 text-right font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<AnimatePresence>{activeKeys.map(renderKeyRow)}</AnimatePresence>
</TableBody>
</Table>
<motion.div layout>
<Table className="p-5">
<TableHeader>
<TableRow>
<TableHead className="pl-5 text-left font-mono uppercase text-xs text-foreground-muted h-auto py-2">
Status
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-muted h-auto py-2">
Key ID
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-muted h-auto py-2">
Created At
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-muted h-auto py-2">
Type
</TableHead>
<TableHead className="pr-5 text-right font-mono uppercase text-xs text-foreground-muted h-auto py-2">
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody>{activeKeys.map(renderKeyRow)}</TableBody>
</Table>
</motion.div>
</CardContent>
</Card>
<Card className="bg-surface-100 mt-3">
<div className="flex">
<div className="bg-surface-200 px-10 flex items-center justify-center">
<StandbyKeyIllustration />
</div>
<div className="flex-1 pl-8 border-l h-full py-6 px-5">
<h4 className="text-sm">Standby JWT signing key</h4>
<p className="text-xs text-foreground-light mb-4 max-w-xs">
Create Standby keys ahead of time which can then be promoted to 'In use' at any
time.
</p>
<Button type="default" icon={<Plus />} onClick={() => setShowCreateKeyDialog(true)}>
Create Standby Key
</Button>
</div>
</div>
</Card>
</div>
{previouslyUsedKeys.length > 0 && (
@@ -697,67 +841,67 @@ NEW_STANDBY_KEY_CONTENT
<DialogHeader>
<DialogTitle>Create a new JWT Key</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="algorithm">Choose the key type to use:</Label>
<Select
value={newKeyAlgorithm}
onValueChange={(value: JWTAlgorithm) => setNewKeyAlgorithm(value)}
>
<SelectTrigger id="algorithm">
<SelectValue placeholder="Select algorithm" />
</SelectTrigger>
<SelectContent>
<SelectItem value="HS256">HS256 (Symmetric)</SelectItem>
<SelectItem value="RS256">RS256 (RSA)</SelectItem>
<SelectItem value="ES256">ES256 (ECC)</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground mt-1">
{algorithmDescriptions[newKeyAlgorithm]}
</p>
</div>
<div>
<Label htmlFor="description">Description:</Label>
<Input
id="description"
value={newKeyDescription}
onChange={(e) => setNewKeyDescription(e.target.value)}
placeholder="Enter key description"
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="useCustomKey"
checked={useCustomSigningKey}
onCheckedChange={(checked) => setUseCustomSigningKey(checked as boolean)}
/>
<Label htmlFor="useCustomKey">Use custom signing key?</Label>
</div>
{useCustomSigningKey && (
<DialogSectionSeparator />
<DialogSection>
<div className="space-y-4">
<div>
<Label htmlFor="customKey">Custom Signing Key:</Label>
<Textarea
id="customKey"
value={customSigningKey}
onChange={(e) => setCustomSigningKey(e.target.value)}
placeholder="Enter your custom signing key"
rows={4}
<Label htmlFor="algorithm">Choose the key type to use:</Label>
<Select
value={newKeyAlgorithm}
onValueChange={(value: JWTAlgorithm) => setNewKeyAlgorithm(value)}
>
<SelectTrigger id="algorithm">
<SelectValue placeholder="Select algorithm" />
</SelectTrigger>
<SelectContent>
<SelectItem value="HS256">HS256 (Symmetric)</SelectItem>
<SelectItem value="RS256">RS256 (RSA)</SelectItem>
<SelectItem value="ES256">ES256 (ECC)</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-foreground-muted mt-1">
{algorithmDetails[newKeyAlgorithm].shortDescription}
</p>
</div>
<div>
<Label htmlFor="description">Description:</Label>
<Input
id="description"
value={newKeyDescription}
onChange={(e) => setNewKeyDescription(e.target.value)}
placeholder="Enter key description"
/>
</div>
)}
{formError && <p className="text-sm text-red-500">{formError}</p>}
</div>
<DialogFooter>
<Button onClick={() => addNewStandbyKey()} disabled={actionInProgress === 'new'}>
{actionInProgress === 'new' ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
'Create'
<div className="flex items-center space-x-2">
<Checkbox
id="useCustomKey"
checked={useCustomSigningKey}
onCheckedChange={(checked) => setUseCustomSigningKey(checked as boolean)}
/>
<Label htmlFor="useCustomKey">Use custom signing key?</Label>
</div>
{useCustomSigningKey && (
<div>
<Label htmlFor="customKey">Custom Signing Key:</Label>
<Textarea
id="customKey"
value={customSigningKey}
onChange={(e) => setCustomSigningKey(e.target.value)}
placeholder="Enter your custom signing key"
rows={4}
/>
</div>
)}
{formError && <p className="text-sm text-red-500">{formError}</p>}
</div>
</DialogSection>
<DialogFooter>
<Button
onClick={() => addNewStandbyKey()}
disabled={actionInProgress === 'new'}
loading={actionInProgress === 'new'}
>
{actionInProgress === 'new' ? <>Creating key...</> : 'Create Standy Signing Key'}
</Button>
</DialogFooter>
</DialogContent>
@@ -784,7 +928,7 @@ NEW_STANDBY_KEY_CONTENT
<span className="font-medium">{key.keyId}</span>
<span className="text-sm text-gray-500">Created: {key.createdAt}</span>
<span className="text-sm text-gray-500">
Type: {algorithmLabels[key.algorithm]}
Type: {algorithmDetails[key.algorithm].label}
</span>
</div>
</SelectItem>
@@ -0,0 +1,81 @@
export interface AlgorithmDetail {
name: string
description: string
pros: string[]
cons: string[]
label: string
shortDescription: string
links: { url: string; label: string }[]
}
export const algorithmDetails: Record<string, AlgorithmDetail> = {
HS256: {
name: 'HMAC with SHA-256',
description: 'Symmetric algorithm using a shared secret key',
pros: [
'Fast and simple to use',
'Requires less computational power',
'Suitable for server-to-server communication',
],
cons: [
'Requires secure key exchange',
"Not suitable when the verifier shouldn't be able to sign tokens",
'Key needs to be kept secret on both sides',
],
label: 'HS256 (Symmetric)',
shortDescription: 'HMAC with SHA-256: Fast, simple, requires secure key exchange',
links: [
{ url: 'https://jwt.io/introduction', label: 'JWT.io Introduction' },
{
url: 'https://datatracker.ietf.org/doc/html/rfc7518#section-3.2',
label: 'RFC 7518 Specification',
},
],
},
RS256: {
name: 'RSA with SHA-256',
description: 'Asymmetric algorithm using a public/private key pair',
pros: [
'Allows public key to be distributed freely',
'Private key can be kept secret on the signing side',
"Suitable for scenarios where the verifier shouldn't be able to sign tokens",
],
cons: [
'Slower than HS256',
'Requires more computational power',
'Keys are larger than ECDSA keys',
],
label: 'RS256 (RSA)',
shortDescription: 'RSA with SHA-256: Allows public key distribution, slower',
links: [
{ url: 'https://jwt.io/introduction', label: 'JWT.io Introduction' },
{
url: 'https://datatracker.ietf.org/doc/html/rfc7518#section-3.3',
label: 'RFC 7518 Specification',
},
],
},
ES256: {
name: 'ECDSA with SHA-256',
description: 'Asymmetric algorithm using elliptic curve cryptography',
pros: [
'Faster than RSA',
'Smaller key and signature sizes compared to RSA',
'Provides forward secrecy',
],
cons: [
'Less widely supported than RSA',
'More complex to implement correctly',
'Requires careful implementation to avoid timing attacks',
],
label: 'ES256 (ECC)',
shortDescription: 'ECDSA with SHA-256: Compact keys, fast, modern alternative to RSA',
links: [
{ url: 'https://jwt.io/introduction', label: 'JWT.io Introduction' },
{
url: 'https://datatracker.ietf.org/doc/html/rfc7518#section-3.4',
label: 'RFC 7518 Specification',
},
],
},
}
+61
View File
@@ -0,0 +1,61 @@
import React from 'react'
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from 'ui/src/components/shadcn/ui/hover-card'
import { Badge } from 'ui/src/components/shadcn/ui/badge'
import { ExternalLink } from 'lucide-react'
interface Link {
url: string
label: string
}
interface InfoPillProps {
label: string
icon: React.ReactNode
title: string
description: string
links?: Link[]
}
export const InfoPill: React.FC<InfoPillProps> = ({ label, icon, title, description, links }) => {
return (
<HoverCard closeDelay={50} openDelay={300}>
<HoverCardTrigger>
<Badge className="gap-2 py-0 hover:border-foreground">
{icon} {label}
</Badge>
</HoverCardTrigger>
<HoverCardContent className="w-80 flex items-start gap-3" side="right" align="center">
<div className="rounded-full bg-surface-300 p-2 flex-shrink-0">
{React.cloneElement(icon as React.ReactElement, {
className: 'w-5 h-5',
strokeWidth: 1.5,
})}
</div>
<div className="flex-1">
<h4 className="text-sm font-semibold truncate">{title}</h4>
<p className="text-xs text-foreground-light">{description}</p>
{links && links.length > 0 && (
<div className="space-y-1 mt-2">
{links.map((link, index) => (
<a
key={index}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-brand hover:text-brand-600 flex items-center"
>
<ExternalLink className="w-3 h-3 mr-1 flex-shrink-0" />
<span className="truncate">{link.label}</span>
</a>
))}
</div>
)}
</div>
</HoverCardContent>
</HoverCard>
)
}