Skip to content

Commit 4afc3bb

Browse files
committed
improvement: logs
1 parent 76981c3 commit 4afc3bb

File tree

13 files changed

+1289
-286
lines changed

13 files changed

+1289
-286
lines changed

apps/sim/app/_shell/dynamic-favicon.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ const SECTION_TO_ICON: Record<string, string> = {
8787
templates: 'templates',
8888
}
8989

90+
function setFaviconHrefs(url: string) {
91+
document.querySelectorAll<HTMLLinkElement>("link[rel*='icon']").forEach((link) => {
92+
if (link.rel === 'apple-touch-icon') return
93+
link.href = url
94+
})
95+
}
96+
9097
export function DynamicFavicon() {
9198
const pathname = usePathname()
9299

@@ -95,17 +102,15 @@ export function DynamicFavicon() {
95102
const iconKey = section ? SECTION_TO_ICON[section] : null
96103
const url = iconKey ? FAVICONS[iconKey] : DEFAULT_FAVICON
97104

98-
const links = document.querySelectorAll<HTMLLinkElement>("link[rel*='icon']")
99-
links.forEach((link) => {
100-
if (link.rel === 'apple-touch-icon') return
101-
link.href = url
102-
})
105+
setFaviconHrefs(url)
106+
107+
// Re-apply whenever Next.js head reconciliation replaces link elements
108+
const observer = new MutationObserver(() => setFaviconHrefs(url))
109+
observer.observe(document.head, { childList: true })
103110

104111
return () => {
105-
links.forEach((link) => {
106-
if (link.rel === 'apple-touch-icon') return
107-
link.href = DEFAULT_FAVICON
108-
})
112+
observer.disconnect()
113+
setFaviconHrefs(DEFAULT_FAVICON)
109114
}
110115
}, [pathname])
111116

apps/sim/app/_shell/hydration-error-handler.tsx

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,41 @@ const BROWSER_EXTENSION_ATTRIBUTES = [
1515
]
1616

1717
/**
18-
* Client component that intercepts console.error to filter and log hydration errors
19-
* while ignoring errors caused by browser extensions.
18+
* Checks whether a hydration error is caused by Radix UI's auto-generated IDs
19+
* (`aria-controls`, `id`) differing between server and client. This is a known
20+
* harmless artifact of React 19's streaming SSR producing different `useId()`
21+
* tree paths. The IDs self-correct on first interaction and have no visual or
22+
* functional impact since they only link closed (invisible) popover/menu content.
23+
*/
24+
function isRadixIdMismatch(args: unknown[]): boolean {
25+
return args.some(
26+
(arg) => typeof arg === 'string' && arg.includes('radix-') && /aria-controls|"\bid\b"/.test(arg)
27+
)
28+
}
29+
30+
/**
31+
* Client component that intercepts console.error to filter hydration errors
32+
* caused by browser extensions or Radix UI's `useId()` mismatch.
2033
*/
2134
export function HydrationErrorHandler() {
2235
useEffect(() => {
2336
const originalError = console.error
2437
console.error = (...args) => {
25-
if (args[0].includes('Hydration')) {
38+
if (typeof args[0] === 'string' && args[0].includes('Hydration')) {
2639
const isExtensionError = BROWSER_EXTENSION_ATTRIBUTES.some((attr) =>
2740
args.some((arg) => typeof arg === 'string' && arg.includes(attr))
2841
)
2942

30-
if (!isExtensionError) {
31-
logger.error('Hydration Error', {
32-
details: args,
33-
componentStack: args.find(
34-
(arg) => typeof arg === 'string' && arg.includes('component stack')
35-
),
36-
})
43+
if (isExtensionError || isRadixIdMismatch(args)) {
44+
return
3745
}
46+
47+
logger.error('Hydration Error', {
48+
details: args,
49+
componentStack: args.find(
50+
(arg) => typeof arg === 'string' && arg.includes('component stack')
51+
),
52+
})
3853
}
3954
originalError.apply(console, args)
4055
}

apps/sim/app/workspace/[workspaceId]/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type {
77
HeaderAction,
88
} from './resource/components/resource-header'
99
export { ResourceHeader } from './resource/components/resource-header'
10+
export type { FilterTag, SearchConfig } from './resource/components/resource-options-bar'
1011
export { ResourceOptionsBar } from './resource/components/resource-options-bar'
1112
export { timeCell } from './resource/components/time-cell/time-cell'
1213
export type { ResourceCell, ResourceColumn, ResourceRow } from './resource/resource'

apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ export function ResourceHeader({
9696
variant='subtle'
9797
className='px-[8px] py-[4px] text-[12px]'
9898
>
99-
{ActionIcon && <ActionIcon className='mr-[6px] h-[14px] w-[14px]' />}
99+
{ActionIcon && (
100+
<ActionIcon className={cn('h-[14px] w-[14px]', action.label && 'mr-[6px]')} />
101+
)}
100102
{action.label}
101103
</Button>
102104
)
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
1-
export type { ColumnOption, SortConfig } from './resource-options-bar'
1+
export type {
2+
ColumnOption,
3+
FilterTag,
4+
SearchConfig,
5+
SearchTag,
6+
SortConfig,
7+
} from './resource-options-bar'
28
export { ResourceOptionsBar } from './resource-options-bar'

apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,41 @@ export interface SortConfig {
3131
onClear?: () => void
3232
}
3333

34+
export interface FilterTag {
35+
label: string
36+
onRemove: () => void
37+
}
38+
39+
export interface SearchTag {
40+
label: string
41+
value: string
42+
onRemove: () => void
43+
}
44+
45+
export interface SearchConfig {
46+
value: string
47+
onChange: (value: string) => void
48+
placeholder?: string
49+
inputRef?: React.RefObject<HTMLInputElement | null>
50+
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void
51+
onFocus?: () => void
52+
onBlur?: () => void
53+
tags?: SearchTag[]
54+
highlightedTagIndex?: number | null
55+
onClearAll?: () => void
56+
dropdown?: ReactNode
57+
dropdownRef?: React.RefObject<HTMLDivElement | null>
58+
}
59+
3460
interface ResourceOptionsBarProps {
35-
search?: {
36-
value: string
37-
onChange: (value: string) => void
38-
placeholder?: string
39-
}
61+
search?: SearchConfig
4062
sort?: SortConfig
4163
filter?: ReactNode
64+
filterTags?: FilterTag[]
4265
}
4366

44-
export function ResourceOptionsBar({ search, sort, filter }: ResourceOptionsBarProps) {
45-
const hasContent = search || sort || filter
67+
export function ResourceOptionsBar({ search, sort, filter, filterTags }: ResourceOptionsBarProps) {
68+
const hasContent = search || sort || filter || (filterTags && filterTags.length > 0)
4669
if (!hasContent) return null
4770

4871
return (
@@ -54,18 +77,67 @@ export function ResourceOptionsBar({ search, sort, filter }: ResourceOptionsBarP
5477
>
5578
<div className='flex items-center justify-between'>
5679
{search && (
57-
<div className='flex flex-1 items-center'>
80+
<div className='relative flex flex-1 items-center'>
5881
<Search className='pointer-events-none h-[14px] w-[14px] shrink-0 text-[var(--text-icon)]' />
59-
<input
60-
type='text'
61-
value={search.value}
62-
onChange={(e) => search.onChange(e.target.value)}
63-
placeholder={search.placeholder ?? 'Search...'}
64-
className='w-full bg-transparent py-[4px] pl-[10px] text-[12px] text-[var(--text-secondary)] outline-none placeholder:text-[var(--text-subtle)]'
65-
/>
82+
<div className='flex flex-1 items-center gap-[6px] overflow-x-auto pl-[10px] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'>
83+
{search.tags?.map((tag, i) => (
84+
<Button
85+
key={`${tag.label}-${tag.value}-${i}`}
86+
variant='subtle'
87+
className={cn(
88+
'shrink-0 px-[8px] py-[4px] text-[12px]',
89+
search.highlightedTagIndex === i &&
90+
'ring-1 ring-[var(--border-focus)] ring-offset-1'
91+
)}
92+
onClick={tag.onRemove}
93+
>
94+
{tag.label}: {tag.value}
95+
<span className='ml-[4px] text-[10px] text-[var(--text-icon)]'></span>
96+
</Button>
97+
))}
98+
<input
99+
ref={search.inputRef}
100+
type='text'
101+
value={search.value}
102+
onChange={(e) => search.onChange(e.target.value)}
103+
onKeyDown={search.onKeyDown}
104+
onFocus={search.onFocus}
105+
onBlur={search.onBlur}
106+
placeholder={search.tags?.length ? '' : (search.placeholder ?? 'Search...')}
107+
className='min-w-[80px] flex-1 bg-transparent py-[4px] text-[12px] text-[var(--text-secondary)] outline-none placeholder:text-[var(--text-subtle)]'
108+
/>
109+
</div>
110+
{search.tags?.length || search.value ? (
111+
<button
112+
type='button'
113+
className='mr-[2px] flex h-[14px] w-[14px] shrink-0 items-center justify-center text-[var(--text-subtle)] transition-colors hover:text-[var(--text-secondary)]'
114+
onClick={search.onClearAll}
115+
>
116+
<span className='text-[12px]'></span>
117+
</button>
118+
) : null}
119+
{search.dropdown && (
120+
<div
121+
ref={search.dropdownRef}
122+
className='absolute top-full left-0 z-50 mt-[6px] w-full rounded-[8px] border border-[var(--border)] bg-white shadow-sm dark:bg-[var(--bg)]'
123+
>
124+
{search.dropdown}
125+
</div>
126+
)}
66127
</div>
67128
)}
68129
<div className='flex items-center gap-[6px]'>
130+
{filterTags?.map((tag) => (
131+
<Button
132+
key={tag.label}
133+
variant='subtle'
134+
className='px-[8px] py-[4px] text-[12px]'
135+
onClick={tag.onRemove}
136+
>
137+
{tag.label}
138+
<span className='ml-[4px] text-[10px] text-[var(--text-icon)]'></span>
139+
</Button>
140+
))}
69141
{filter && (
70142
<PopoverPrimitive.Root>
71143
<PopoverPrimitive.Trigger asChild>

0 commit comments

Comments
 (0)