LogsFormatters.tsx252 lines · main
| 1 | /* |
| 2 | * Response Code |
| 3 | * |
| 4 | * for http response codes |
| 5 | */ |
| 6 | |
| 7 | import dayjs from 'dayjs' |
| 8 | import { AlertCircle, Info } from 'lucide-react' |
| 9 | import React from 'react' |
| 10 | import { cn } from 'ui' |
| 11 | |
| 12 | import { isUnixMicro, unixMicroToIsoTimestamp } from './Logs.utils' |
| 13 | import CopyButton from '@/components/ui/CopyButton' |
| 14 | |
| 15 | export const RowLayout: React.FC<React.PropsWithChildren> = ({ children }) => ( |
| 16 | <div className="flex h-full w-full items-center gap-4">{children}</div> |
| 17 | ) |
| 18 | // renders a timestamp (either unix microsecond or iso timestamp) |
| 19 | export const SelectionDetailedTimestampRow = ({ value }: { value: string | number }) => ( |
| 20 | <SelectionDetailedRow |
| 21 | label="Timestamp" |
| 22 | value={isUnixMicro(value) ? unixMicroToIsoTimestamp(value) : String(value)} |
| 23 | /> |
| 24 | ) |
| 25 | export const SelectionDetailedRow = ({ |
| 26 | label, |
| 27 | value, |
| 28 | valueRender, |
| 29 | }: { |
| 30 | label: string |
| 31 | value: string |
| 32 | valueRender?: React.ReactNode |
| 33 | }) => { |
| 34 | return ( |
| 35 | <div className="group flex items-center gap-2 flex-wrap"> |
| 36 | <span className="text-foreground-lighter text-sm col-span-3 whitespace-pre-wrap"> |
| 37 | {label} |
| 38 | </span> |
| 39 | <span |
| 40 | title={value} |
| 41 | className="truncate font-mono text-foreground text-sm whitespace-pre-wrap break-all" |
| 42 | > |
| 43 | {valueRender ?? value} |
| 44 | </span> |
| 45 | <CopyButton |
| 46 | iconOnly |
| 47 | text={value} |
| 48 | className="group-hover:opacity-100 opacity-0 p-0 h-6 w-6" |
| 49 | type="text" |
| 50 | title="Copy to clipboard" |
| 51 | /> |
| 52 | </div> |
| 53 | ) |
| 54 | } |
| 55 | |
| 56 | // used for column renderers |
| 57 | export const TextFormatter: React.FC<{ className?: string; value: string }> = ({ |
| 58 | value, |
| 59 | className, |
| 60 | }) => ( |
| 61 | <span className={cn('font-mono text-xs truncate select-text cursor-text', className)}> |
| 62 | {value} |
| 63 | </span> |
| 64 | ) |
| 65 | |
| 66 | export const ResponseCodeFormatter = ({ value }: { value: string }) => { |
| 67 | if (!value) { |
| 68 | return ( |
| 69 | <div> |
| 70 | <label className="text-xs text-border-stronger">No data</label> |
| 71 | </div> |
| 72 | ) |
| 73 | } |
| 74 | |
| 75 | const ResponseCodeItem = ({ |
| 76 | children, |
| 77 | className, |
| 78 | }: { |
| 79 | children: React.ReactNode |
| 80 | className?: string |
| 81 | }) => ( |
| 82 | <div className="flex h-full items-center"> |
| 83 | <div |
| 84 | className={cn( |
| 85 | `relative flex h-6 items-center rounded-md justify-center px-2 py-1 text-center`, |
| 86 | className |
| 87 | )} |
| 88 | > |
| 89 | <label className="block font-mono text-sm">{children}</label> |
| 90 | </div> |
| 91 | </div> |
| 92 | ) |
| 93 | |
| 94 | const split = value.toString().split('')[0] |
| 95 | |
| 96 | switch (split) { |
| 97 | // 2XX || 1XX responses |
| 98 | case '1': |
| 99 | return <ResponseCodeItem>{value}</ResponseCodeItem> |
| 100 | case '2': |
| 101 | return <ResponseCodeItem className="bg-surface-100 text-brand">{value}</ResponseCodeItem> |
| 102 | // 5XX responses |
| 103 | case '5': |
| 104 | return <ResponseCodeItem className="bg-red-300 text-red-1100">{value}</ResponseCodeItem> |
| 105 | |
| 106 | // 4XX || 3XX responses |
| 107 | case '4': |
| 108 | case '3': |
| 109 | return <ResponseCodeItem className="bg-warning/10 text-warning">{value}</ResponseCodeItem> |
| 110 | |
| 111 | // All other responses |
| 112 | default: |
| 113 | return ( |
| 114 | <ResponseCodeItem className="bg-surface-100 text-foreground-lighter"> |
| 115 | {value} |
| 116 | </ResponseCodeItem> |
| 117 | ) |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | /* |
| 122 | * Response Code |
| 123 | * |
| 124 | * for http response codes |
| 125 | */ |
| 126 | |
| 127 | export const SeverityFormatter = ({ |
| 128 | value, |
| 129 | uppercase = true, |
| 130 | }: { |
| 131 | value: string |
| 132 | uppercase?: boolean |
| 133 | }) => { |
| 134 | if (!value) { |
| 135 | return ( |
| 136 | <div> |
| 137 | <label className="text-xs text-border-stronger">No data</label> |
| 138 | </div> |
| 139 | ) |
| 140 | } |
| 141 | |
| 142 | const uppercasedValue = value.toUpperCase() |
| 143 | const text = uppercase ? uppercasedValue : value |
| 144 | const Layout: React.FC<React.PropsWithChildren<{ className?: string }>> = ({ |
| 145 | className, |
| 146 | children, |
| 147 | }) => <div className={`w-24 flex items-center h-full ${className}`}>{children}</div> |
| 148 | |
| 149 | switch (uppercasedValue) { |
| 150 | case 'UNCAUGHTEXCEPTION': |
| 151 | case 'PANIC': |
| 152 | case 'FATAL': |
| 153 | case 'ERROR': |
| 154 | return ( |
| 155 | <Layout className="gap-1"> |
| 156 | <div className=" p-0.5 rounded-sm text-red-900!"> |
| 157 | <AlertCircle size={14} strokeWidth={2} /> |
| 158 | </div> |
| 159 | <span className="text-red-900! block! titlecase">{text}</span> |
| 160 | </Layout> |
| 161 | ) |
| 162 | break |
| 163 | |
| 164 | case 'INFO': |
| 165 | |
| 166 | case 'DEBUG': |
| 167 | return ( |
| 168 | <Layout className="gap-1"> |
| 169 | <div className=" p-0.5 rounded-sm text-blue-900!"> |
| 170 | <AlertCircle size={14} strokeWidth={2} /> |
| 171 | </div> |
| 172 | <span className="text-blue-900! block! titlecase">{text}</span> |
| 173 | </Layout> |
| 174 | ) |
| 175 | break |
| 176 | |
| 177 | case 'LOG': |
| 178 | return ( |
| 179 | <Layout className="gap-1"> |
| 180 | <div className=" p-0.5 rounded-sm text-blue-900!"> |
| 181 | <Info size={14} strokeWidth={2} /> |
| 182 | </div> |
| 183 | <span className="text-blue-900! block! titlecase">{text}</span> |
| 184 | </Layout> |
| 185 | ) |
| 186 | break |
| 187 | |
| 188 | case 'WARNING': |
| 189 | return ( |
| 190 | <Layout className="gap-1"> |
| 191 | <div className=" p-0.5 rounded-sm text-amber-900!"> |
| 192 | <AlertCircle size={14} strokeWidth={2} /> |
| 193 | </div> |
| 194 | <span className="text-amber-900! block! titlecase">{text}</span> |
| 195 | </Layout> |
| 196 | ) |
| 197 | break |
| 198 | |
| 199 | // All other responses |
| 200 | default: |
| 201 | return ( |
| 202 | <Layout> |
| 203 | <div className="relative rounded-sm px-2 py-1 text-center h-6 flex justify-center items-center bg-surface-100"> |
| 204 | <label className="block font-mono text-sm text-foreground-lighter">{text}</label> |
| 205 | </div> |
| 206 | </Layout> |
| 207 | ) |
| 208 | break |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | export const timestampLocalFormatter = (value: string | number) => { |
| 213 | const timestamp = isUnixMicro(value) ? unixMicroToIsoTimestamp(value) : value |
| 214 | return dayjs(timestamp).format('DD MMM HH:mm:ss') |
| 215 | } |
| 216 | |
| 217 | /* |
| 218 | * JSON Syntax Highlighter |
| 219 | * |
| 220 | * for http response codes |
| 221 | */ |
| 222 | |
| 223 | export function jsonSyntaxHighlight(input: Object) { |
| 224 | let json: string = JSON.stringify(input, null, 2) |
| 225 | json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') |
| 226 | |
| 227 | const newJson = json.replace( |
| 228 | /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, |
| 229 | |
| 230 | function (match) { |
| 231 | var cls = 'number text-tomato-900' |
| 232 | if (/^"/.test(match)) { |
| 233 | if (/:$/.test(match)) { |
| 234 | cls = 'key text-foreground' |
| 235 | } else { |
| 236 | cls = 'string text-brand-600' |
| 237 | } |
| 238 | } else if (/true|false/.test(match)) { |
| 239 | cls = 'boolean text-blue-900' |
| 240 | } else if (/null/.test(match)) { |
| 241 | cls = 'null text-amber-1100' |
| 242 | } |
| 243 | return '<span class="' + cls + '">' + match + '</span>' |
| 244 | } |
| 245 | ) |
| 246 | |
| 247 | const jsonWithLineWraps = newJson.split(`\n`).map((x) => { |
| 248 | return `<span class="line text-xs">${x}</span>` |
| 249 | }) |
| 250 | |
| 251 | return jsonWithLineWraps.join('\n') |
| 252 | } |