Skip to content

Commit b10ed6c

Browse files
committed
improvement(org): simplify Home after review
Drops what only fixtures needed, keeps the dashboard from re-rendering on each keystroke, and shares the authorized-app paging and calendar-date check. Connect now answers to both gates the widget's read applies, and readiness moves to a module free of the connector catalog so the extension's graph stays as it was.
1 parent fdbedb4 commit b10ed6c

25 files changed

Lines changed: 362 additions & 341 deletions

File tree

‎apps/sim/app/o/[organizationId]/home/components/get-started/get-started.tsx‎

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
'use client'
22

3-
import { useEffect, useState } from 'react'
3+
import { useState } from 'react'
44
import { cn } from '@sim/emcn'
55
import { ArrowRight } from '@sim/emcn/icons'
66
import { HomeSection } from '@/components/home/home-section'
77
import { SettingsGuardedLink } from '@/components/settings/settings-guarded-link'
8-
import { OAUTH_SEARCH_READ_SCOPE, oauthScopeSatisfies } from '@/lib/auth/oauth-provider'
9-
import { hasSearchIntegrationReady } from '@/lib/credential-groups/member-integrations'
8+
import { hasSearchIntegrationReady } from '@/lib/credential-groups/search-readiness'
109
import { organizationRoutes } from '@/lib/navigation/paths'
1110
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
12-
import { useAuthorizedApps } from '@/hooks/queries/oauth-provider'
11+
import { useHasSearchAuthorization } from '@/hooks/queries/oauth-provider'
1312
import { useOrganizationAccounts } from '@/hooks/queries/organization-accounts'
1413
import { useSearchIntegrations } from '@/hooks/queries/search-integrations'
1514

@@ -80,17 +79,8 @@ export function GetStarted() {
8079
const { data: integrations } = useSearchIntegrations(organization.id, {
8180
enabled: canConnectIntegrations,
8281
})
83-
const {
84-
data: authorizedApps,
85-
fetchNextPage,
86-
hasNextPage,
87-
isFetching,
88-
isError,
89-
} = useAuthorizedApps('', { enabled: viewer.canUseSearchMcp })
90-
const hasSearchAuthorization =
91-
authorizedApps?.pages.some((page) =>
92-
page.apps.some((app) => oauthScopeSatisfies(app.scopes, OAUTH_SEARCH_READ_SCOPE))
93-
) ?? false
82+
const hasSearchAuthorization = useHasSearchAuthorization(viewer.canUseSearchMcp)
83+
9484
const hrefs: Record<StepId, string> = {
9585
'connect-integration': viewer.isAdmin
9686
? routes.settingsSection('integrations')
@@ -114,25 +104,6 @@ export function GetStarted() {
114104
*/
115105
const [animationsEnabled, setAnimationsEnabled] = useState(false)
116106

117-
useEffect(() => {
118-
if (
119-
viewer.canUseSearchMcp &&
120-
!hasSearchAuthorization &&
121-
hasNextPage &&
122-
!isFetching &&
123-
!isError
124-
) {
125-
void fetchNextPage()
126-
}
127-
}, [
128-
viewer.canUseSearchMcp,
129-
hasSearchAuthorization,
130-
hasNextPage,
131-
isFetching,
132-
isError,
133-
fetchNextPage,
134-
])
135-
136107
const handleToggleExpanded = () => {
137108
setAnimationsEnabled(true)
138109
setExpanded((prev) => !prev)

‎apps/sim/app/o/[organizationId]/home/page.tsx‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,13 @@ interface PrefetchedHomeProps {
2121
surface: OrganizationSurfaceContext
2222
principal: SessionPrincipal
2323
requestMode: 'agent' | 'assistant'
24-
userName: string
2524
}
2625

2726
/** Holds only its own boundary, so the page's arrangement paints before these reads settle. */
28-
async function PrefetchedHome({ surface, principal, requestMode, userName }: PrefetchedHomeProps) {
27+
async function PrefetchedHome({ surface, principal, requestMode }: PrefetchedHomeProps) {
2928
return (
3029
<HydrationBoundary state={await prefetchHome(surface, principal)}>
31-
<HomeConversation requestMode={requestMode} userName={userName} />
30+
<HomeConversation requestMode={requestMode} />
3231
</HydrationBoundary>
3332
)
3433
}
@@ -53,7 +52,6 @@ export default async function OrganizationHomePage({
5352
surface={context}
5453
principal={{ kind: 'session', userId: session.user.id, sessionId: session.session.id }}
5554
requestMode={requestMode}
56-
userName={session.user.name}
5755
/>
5856
</Suspense>
5957
)

‎apps/sim/ee/organization-usage/hooks/use-usage-window.ts‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from '@/lib/api/contracts/organization-usage'
88
import { ACTIVITY_MAX_PAGE } from '@/lib/billing/core/organization-activity'
99
import { formatDateShort } from '@/lib/core/utils/date-display'
10-
import { getBrowserTimezone } from '@/lib/core/utils/timezone'
10+
import { getBrowserTimezone, isCalendarDate } from '@/lib/core/utils/timezone'
1111
import { DEFAULT_USAGE_PRESET, PERIOD_LABELS } from '@/ee/organization-usage/constants'
1212
import {
1313
organizationUsageParsers,
@@ -17,12 +17,6 @@ import type { OrganizationUsageWindowKey } from '@/hooks/queries/utils/organizat
1717

1818
const DAY_MS = 24 * 60 * 60 * 1000
1919

20-
/** A bare `YYYY-MM-DD` that survives a calendar round-trip — the contract's rule, verbatim. */
21-
function isCalendarDate(value: string | null): value is string {
22-
if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false
23-
return new Date(`${value}T00:00:00.000Z`).toISOString().slice(0, 10) === value
24-
}
25-
2620
/** Match server date validation so invalid links fall back before issuing requests. */
2721
export function isUsableCustomRange(start: string | null, end: string | null): boolean {
2822
if (!isCalendarDate(start) || !isCalendarDate(end)) return false

‎apps/sim/hooks/queries/oauth-provider.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useEffect } from 'react'
12
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
23
import { requestJson } from '@/lib/api/client/request'
34
import {
@@ -6,6 +7,7 @@ import {
67
revokeAuthorizedAppContract,
78
} from '@/lib/api/contracts/user'
89
import { client } from '@/lib/auth/client'
10+
import { OAUTH_SEARCH_READ_SCOPE, oauthScopeSatisfies } from '@/lib/auth/oauth-provider'
911

1012
export const oauthProviderKeys = {
1113
all: ['oauth-provider'] as const,
@@ -111,3 +113,17 @@ export function useOAuthSwitchAccount() {
111113
},
112114
})
113115
}
116+
117+
/** Whether an app the viewer authorized can read Search, paging until one is found or none is left. */
118+
export function useHasSearchAuthorization(enabled: boolean): boolean {
119+
const apps = useAuthorizedApps('', { enabled })
120+
const authorized =
121+
apps.data?.pages.some((page) =>
122+
page.apps.some((app) => oauthScopeSatisfies(app.scopes, OAUTH_SEARCH_READ_SCOPE))
123+
) ?? false
124+
const { hasNextPage, isFetching, isError, fetchNextPage } = apps
125+
useEffect(() => {
126+
if (enabled && !authorized && hasNextPage && !isFetching && !isError) void fetchNextPage()
127+
}, [enabled, authorized, hasNextPage, isFetching, isError, fetchNextPage])
128+
return authorized
129+
}

‎apps/sim/hooks/queries/organization-home.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function useOrganizationHomeEmail(
3232
queryKey: organizationHomeKeys.email(organizationId),
3333
queryFn: ({ signal }) =>
3434
requestJson(getOrganizationHomeEmailContract, { params: { id: organizationId }, signal }),
35-
enabled: Boolean(organizationId) && enabled,
35+
enabled,
3636
staleTime: EMAIL_STALE_TIME,
3737
refetchOnWindowFocus: true,
3838
})
@@ -52,7 +52,7 @@ export function useOrganizationHomeCalendar(
5252
query: { date, timeZone },
5353
signal,
5454
}),
55-
enabled: Boolean(organizationId && date) && enabled,
55+
enabled,
5656
staleTime: CALENDAR_STALE_TIME,
5757
refetchOnWindowFocus: true,
5858
})

‎apps/sim/lib/core/utils/timezone.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ const COMMON_TIMEZONES = [
2323
'Australia/Sydney',
2424
]
2525

26+
/** A bare `yyyy-MM-dd` that survives a calendar round-trip, so `2026-02-31` is not a date. */
27+
export function isCalendarDate(value: string | null): value is string {
28+
if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false
29+
const instant = new Date(`${value}T00:00:00.000Z`)
30+
return !Number.isNaN(instant.getTime()) && instant.toISOString().slice(0, 10) === value
31+
}
32+
2633
/** Adds whole days to a `yyyy-MM-dd` date. Anchored at noon so no zone or DST can shift the day. */
2734
export function addCalendarDays(date: string, days: number): string {
2835
const next = new Date(`${date}T12:00:00Z`)

‎apps/sim/lib/credential-groups/member-integrations.ts‎

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ import {
99
} from '@/lib/credential-groups/providers'
1010
import { hasSlackSearchUserScopes } from '@/lib/credential-groups/slack-managed-user-scopes'
1111
import { LIVE_SEARCH_SCOPE_FIELDS } from '@/lib/sim-search/live/policy-schema'
12-
import {
13-
liveSearchProviderForCredential,
14-
supportsLiveSearchMode,
15-
} from '@/lib/sim-search/live/provider-catalog'
1612
import {
1713
LIVE_SEARCH_SOURCE_TYPES,
1814
liveSearchMcpConnector,
@@ -40,52 +36,6 @@ export function canReadMemberIntegrations(surface: {
4036
)
4137
}
4238

43-
/** Get started's first step: the viewer reads Search through their own connection or a source. */
44-
export function hasSearchIntegrationReady(
45-
accounts: OrganizationAccountsSettings | undefined,
46-
policies: SearchIntegrationStatus[] | undefined
47-
): boolean {
48-
const usable = policies?.filter((policy) => policy.approved && policy.available !== false)
49-
const approved = new Set(usable?.map((policy) => policy.connectorType))
50-
const group = accounts?.credentialGroup
51-
const readyOptions = new Set(
52-
group?.options
53-
.filter((option) => {
54-
const provider = liveSearchProviderForCredential(option.provider)
55-
return (
56-
option.status === 'active' &&
57-
option.configurationStatus === 'ready' &&
58-
provider &&
59-
supportsLiveSearchMode(provider, 'member') &&
60-
approved.has(provider)
61-
)
62-
})
63-
.map((option) => option.id)
64-
)
65-
const readyServers = new Set(
66-
group?.mcpServers
67-
.filter((server) => {
68-
const provider = liveSearchProviderForCredential(`mcp:${server.managedConnectorId}`)
69-
return (
70-
server.enabled &&
71-
provider &&
72-
approved.has(provider) &&
73-
accounts?.availableMcpConnectors.some((id) => id === server.managedConnectorId)
74-
)
75-
})
76-
.map((server) => server.id)
77-
)
78-
const hasConnection =
79-
group?.status === 'active' &&
80-
(accounts?.viewerAccounts?.some(
81-
(account) => account.status === 'active' && readyOptions.has(account.optionId)
82-
) ||
83-
accounts?.viewerMcpAccounts?.some(
84-
(account) => account.status === 'active' && readyServers.has(account.mcpServerId)
85-
))
86-
return Boolean(hasConnection || usable?.some((policy) => policy.configuredServiceSource))
87-
}
88-
8939
export function assistantAccountOptions(data: OrganizationAccountsSettings, search: string) {
9040
const providers = data.assistantProviders.filter(
9141
(provider) => !SEARCH_ACCOUNT_PROVIDERS.has(provider)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { SearchIntegrationStatus } from '@/lib/api/contracts/knowledge/search-integrations'
2+
import type { OrganizationAccountsSettings } from '@/lib/api/contracts/organization-accounts'
3+
import {
4+
liveSearchProviderForCredential,
5+
supportsLiveSearchMode,
6+
} from '@/lib/sim-search/live/provider-catalog'
7+
8+
/** Get started's first step: the viewer reads Search through their own connection or a source. */
9+
export function hasSearchIntegrationReady(
10+
accounts: OrganizationAccountsSettings | undefined,
11+
policies: SearchIntegrationStatus[] | undefined
12+
): boolean {
13+
const usable = policies?.filter((policy) => policy.approved && policy.available !== false)
14+
const approved = new Set(usable?.map((policy) => policy.connectorType))
15+
const group = accounts?.credentialGroup
16+
const readyOptions = new Set(
17+
group?.options
18+
.filter((option) => {
19+
const provider = liveSearchProviderForCredential(option.provider)
20+
return (
21+
option.status === 'active' &&
22+
option.configurationStatus === 'ready' &&
23+
provider &&
24+
supportsLiveSearchMode(provider, 'member') &&
25+
approved.has(provider)
26+
)
27+
})
28+
.map((option) => option.id)
29+
)
30+
const readyServers = new Set(
31+
group?.mcpServers
32+
.filter((server) => {
33+
const provider = liveSearchProviderForCredential(`mcp:${server.managedConnectorId}`)
34+
return (
35+
server.enabled &&
36+
provider &&
37+
approved.has(provider) &&
38+
accounts?.availableMcpConnectors.some((id) => id === server.managedConnectorId)
39+
)
40+
})
41+
.map((server) => server.id)
42+
)
43+
const hasConnection =
44+
group?.status === 'active' &&
45+
(accounts?.viewerAccounts?.some(
46+
(account) => account.status === 'active' && readyOptions.has(account.optionId)
47+
) ||
48+
accounts?.viewerMcpAccounts?.some(
49+
(account) => account.status === 'active' && readyServers.has(account.mcpServerId)
50+
))
51+
return Boolean(hasConnection || usable?.some((policy) => policy.configuredServiceSource))
52+
}

‎apps/sim/platform/app/(shell)/home/_components/activity-chart/activity-chart.tsx‎

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
import { useState } from 'react'
44
import styles from '@/platform/app/(shell)/home/_components/activity-chart/activity-chart.module.css'
5-
import type { HomeActivityState } from '@/platform/app/(shell)/home/_components/model'
5+
import type {
6+
ActivityPoint,
7+
HomeActivityState,
8+
} from '@/platform/app/(shell)/home/_components/model'
69
import { RetryButton } from '@/platform/app/(shell)/home/_components/retry-button/retry-button'
710
import { SkeletonBlock } from '@/platform/app/(shell)/home/_components/widget-skeleton/widget-skeleton'
811
import { TrendPlot } from '@/platform/design-system/catalog/plots'
@@ -12,18 +15,12 @@ import { ChartTooltip } from '@/platform/design-system/components/chart/chart-to
1215
import { usePlotInspection } from '@/platform/design-system/components/chart/use-plot-inspection'
1316
import { RangePicker } from '@/platform/design-system/components/menu/range-picker'
1417
import { useScrollEdges } from '@/platform/design-system/components/scroll-area/use-scroll-edges'
15-
import { DAY_MS, formatCalendarDate } from '@/platform/lib/time'
16-
17-
interface ActivityPoint {
18-
date: string
19-
value: number
20-
}
18+
import { formatCalendarDate } from '@/platform/lib/time'
2119

2220
interface HomeActivityChartProps {
2321
title: string
2422
state: HomeActivityState
25-
onRetry?: () => void
26-
hidden?: boolean
23+
onRetry: () => void
2724
}
2825

2926
interface ActivitySeriesProps {
@@ -93,33 +90,13 @@ function ActivitySeries({ title, points }: ActivitySeriesProps) {
9390
const WINDOW_DAYS = [7, 30, 90] as const
9491
const RANGE_LABELS = WINDOW_DAYS.map((days) => `Last ${days} days`)
9592

96-
function calendarWindow(history: readonly ActivityPoint[] | undefined, days: number) {
97-
if (!history?.length) return []
98-
const end = Date.parse(history[history.length - 1].date)
99-
const start = Math.max(Date.parse(history[0].date), end - (days - 1) * DAY_MS)
100-
const counts = new Map(history.map(({ date, value }) => [date, value]))
101-
const points: ActivityPoint[] = []
102-
for (let day = start; day <= end; day += DAY_MS) {
103-
const date = new Date(day).toISOString().slice(0, 10)
104-
const count = counts.get(date) ?? 0
105-
points.push({ date, value: Number.isFinite(count) && count > 0 ? count : 0 })
106-
}
107-
return points
108-
}
109-
110-
export function HomeActivityChart({
111-
title,
112-
state,
113-
onRetry,
114-
hidden = false,
115-
}: HomeActivityChartProps) {
93+
export function HomeActivityChart({ title, state, onRetry }: HomeActivityChartProps) {
11694
const [range, setRange] = useState(RANGE_LABELS[0])
11795
const scrollEdges = useScrollEdges<HTMLElement>('vertical', { tabStop: true })
118-
if (hidden || state.status === 'hidden') return null
96+
if (state.status === 'hidden') return null
11997
const ready = state.status === 'ready'
120-
const history = ready ? state.points : undefined
12198
const days = WINDOW_DAYS[RANGE_LABELS.indexOf(range)]
122-
const points = calendarWindow(history, days)
99+
const points = ready ? state.points.slice(-days) : []
123100
const total = points.reduce((sum, point) => sum + point.value, 0)
124101
return (
125102
<section ref={scrollEdges} className={styles.chart} aria-label={title}>
@@ -135,7 +112,7 @@ export function HomeActivityChart({
135112
{ready && (
136113
<RangePicker
137114
label='Range'
138-
options={history ? RANGE_LABELS : []}
115+
options={RANGE_LABELS}
139116
value={range}
140117
onValueChange={setRange}
141118
/>

0 commit comments

Comments
 (0)