Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/sim/app/_shell/application-loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { cn, SimWordmark } from '@sim/emcn'
import { DesktopTitleBarLane } from '@/app/_shell/desktop-title-bar'
import { type BrandConfig, getBrandConfig } from '@/ee/whitelabeling'

interface ApplicationLoadingProps {
brand?: BrandConfig
fullScreen?: boolean
}

/** Shared loading surface for app entry, shell prefetch, and workflow navigation. */
export function ApplicationLoading({
brand = getBrandConfig(),
fullScreen = true,
}: ApplicationLoadingProps) {
const wordmarkUrl = brand.wordmarkUrl || brand.logoUrl

return (
<div
role='status'
aria-label={`Loading ${brand.name}`}
className={cn(
'flex w-full items-center justify-center bg-[var(--bg)]',
fullScreen ? 'desktop-title-bar-page' : 'h-full'
)}
>
{fullScreen && <DesktopTitleBarLane />}
{wordmarkUrl ? (
<img src={wordmarkUrl} alt='' className='h-8 max-w-[240px] object-contain' />
) : brand.isWhitelabeled ? (
<span className='text-[var(--text-tertiary)] text-lg'>{brand.name}</span>
) : (
<SimWordmark size='loading' tone='brand-muted' />
)}
</div>
)
}
5 changes: 5 additions & 0 deletions apps/sim/app/account/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ApplicationLoading } from '@/app/_shell/application-loading'

export default function Loading() {
return <ApplicationLoading />
}
5 changes: 5 additions & 0 deletions apps/sim/app/home/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ApplicationLoading } from '@/app/_shell/application-loading'

export default function Loading() {
return <ApplicationLoading />
}
5 changes: 5 additions & 0 deletions apps/sim/app/o/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ApplicationLoading } from '@/app/_shell/application-loading'

export default function Loading() {
return <ApplicationLoading />
}
5 changes: 5 additions & 0 deletions apps/sim/app/selfhost/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ApplicationLoading } from '@/app/_shell/application-loading'

export default function Loading() {
return <ApplicationLoading />
}
8 changes: 4 additions & 4 deletions apps/sim/app/workspace/[workspaceId]/layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createSessionPrincipal } from '@sim/testing/factories/principal.factory
import { emcnMock } from '@sim/testing/mocks/emcn.mock'
import { nextNavigationMock } from '@sim/testing/mocks/next-navigation.mock'
import { reactQueryMock } from '@sim/testing/mocks/react-query.mock'
import { renderToStaticMarkup } from 'react-dom/server'
import { renderToReadableStream } from 'react-dom/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const {
Expand Down Expand Up @@ -178,7 +178,7 @@ describe('WorkspaceLayout host context', () => {
children: <div>Workspace child</div>,
params: Promise.resolve({ workspaceId: 'workspace-b' }),
})
renderToStaticMarkup(element)
await new Response(await renderToReadableStream(element)).text()

expect(mockGetOrgWhitelabelSettings).toHaveBeenCalledWith('org-b')
expect(mockGetOrgWhitelabelSettings).not.toHaveBeenCalledWith('org-a')
Expand Down Expand Up @@ -211,7 +211,7 @@ describe('WorkspaceLayout host context', () => {
children: <div>Secret workspace child</div>,
params: Promise.resolve({ workspaceId: 'workspace-denied' }),
})
const html = renderToStaticMarkup(element)
const html = await new Response(await renderToReadableStream(element)).text()

expect(html).toContain('Workspace access denied')
expect(html).not.toContain('Secret workspace child')
Expand All @@ -226,7 +226,7 @@ describe('WorkspaceLayout host context', () => {
children: <div>Workspace child</div>,
params: Promise.resolve({ workspaceId: 'workspace-b' }),
})
renderToStaticMarkup(element)
await new Response(await renderToReadableStream(element)).text()
return mockWorkspaceChrome.mock.calls[0][0].sidebar
}

Expand Down
68 changes: 59 additions & 9 deletions apps/sim/app/workspace/[workspaceId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { Suspense } from 'react'
import { dehydrate, HydrationBoundary, type QueryClient } from '@tanstack/react-query'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { SettingsNavigationProvider } from '@/components/settings/settings-navigation-provider'
import type { WorkspaceHostContext } from '@/lib/api/contracts/workspaces'
import { getSession } from '@/lib/auth'
import { getActiveOrganizationId } from '@/lib/auth/session-response'
import { isChangelogEnabled } from '@/lib/changelog/feature-flag'
Expand All @@ -13,6 +15,7 @@ import {
import { isMothershipModelSelectorEnabled, isPlanModeEnabled } from '@/lib/mothership/feature-flags'
import { resolveOrganizationEntryPath } from '@/lib/navigation/resolve-app-entry'
import { isWorkflowTestsEnabled } from '@/lib/workflow-tests/feature-flag'
import { ApplicationLoading } from '@/app/_shell/application-loading'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import { ImpersonationBanner } from '@/app/workspace/[workspaceId]/components/impersonation-banner'
import { SessionExpired } from '@/app/workspace/[workspaceId]/components/session-expired'
Expand All @@ -38,16 +41,33 @@ import { WorkspaceHostProvider } from '@/app/workspace/[workspaceId]/providers/w
import { WorkspacePermissionsProvider } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { WorkspaceScopeSync } from '@/app/workspace/[workspaceId]/providers/workspace-scope-sync'
import { Sidebar } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
import {
getBrandConfig,
mergeOrgBrandConfig,
type OrganizationWhitelabelSettings,
} from '@/ee/whitelabeling'
import { BrandingProvider } from '@/ee/whitelabeling/components/branding-provider'
import { getOrgWhitelabelSettings } from '@/ee/whitelabeling/org-branding'

export default async function WorkspaceLayout({
children,
params,
}: {
interface WorkspaceLayoutProps {
children: React.ReactNode
params: Promise<{ workspaceId: string }>
}) {
}

interface WorkspaceContentProps {
children: React.ReactNode
workspaceId: string
session: NonNullable<Awaited<ReturnType<typeof getSession>>>
queryClient: QueryClient
hostContext: WorkspaceHostContext
orgSettings: Promise<OrganizationWhitelabelSettings | null>
}

interface WorkspaceLoadingProps {
orgSettings: Promise<OrganizationWhitelabelSettings | null>
}

export default async function WorkspaceLayout({ children, params }: WorkspaceLayoutProps) {
const session = await getSession()
if (!session?.user) {
redirect('/login')
Expand All @@ -60,6 +80,38 @@ export default async function WorkspaceLayout({
return <WorkspaceAccessDenied />
}

const orgSettings = hostContext.hostOrganizationId
? getOrgWhitelabelSettings(hostContext.hostOrganizationId)
: Promise.resolve(null)

return (
<Suspense fallback={<WorkspaceLoading orgSettings={orgSettings} />}>
<WorkspaceContent
workspaceId={workspaceId}
session={session}
queryClient={queryClient}
hostContext={hostContext}
orgSettings={orgSettings}
>
{children}
</WorkspaceContent>
</Suspense>
)
}

async function WorkspaceLoading({ orgSettings }: WorkspaceLoadingProps) {
const brand = mergeOrgBrandConfig(await orgSettings, getBrandConfig())
return <ApplicationLoading brand={brand} />
}

async function WorkspaceContent({
children,
workspaceId,
session,
queryClient,
hostContext,
orgSettings,
}: WorkspaceContentProps) {
const activeOrganizationId = getActiveOrganizationId(session)
const principal = {
kind: 'session',
Expand All @@ -79,9 +131,7 @@ export default async function WorkspaceLayout({
desktopExecutorRegistered,
] = await Promise.all([
cookies(),
hostContext.hostOrganizationId
? getOrgWhitelabelSettings(hostContext.hostOrganizationId)
: Promise.resolve(null),
orgSettings,
prefetchWorkspaceSidebar(
queryClient,
workspaceId,
Expand Down
20 changes: 4 additions & 16 deletions apps/sim/app/workspace/[workspaceId]/w/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,17 @@ import { Chip } from '@sim/emcn'
import { createLogger } from '@sim/logger'
import { ReactFlowProvider } from '@xyflow/react'
import { useParams, useRouter } from 'next/navigation'
import { ApplicationLoading } from '@/app/_shell/application-loading'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { Panel, Terminal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components'
import { useWorkflowOperations } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import { useOrgBrandConfig } from '@/ee/whitelabeling/components/branding-provider'
import { useWorkflows } from '@/hooks/queries/workflows'

const logger = createLogger('WorkflowsPage')

function Spinner() {
return (
<div
className='size-[18px] animate-spin rounded-full'
style={{
background:
'conic-gradient(from 0deg, var(--text-icon) 0deg 120deg, transparent 120deg 180deg, var(--text-icon) 180deg 300deg, transparent 300deg 360deg)',
mask: 'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
WebkitMask:
'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
}}
/>
)
}

export default function WorkflowsPage() {
const brand = useOrgBrandConfig()
const router = useRouter()
const params = useParams()
const workspaceId = params.workspaceId as string
Expand Down Expand Up @@ -104,7 +92,7 @@ export default function WorkflowsPage() {
)}
</div>
) : (
<Spinner />
<ApplicationLoading brand={brand} fullScreen={false} />
)}
</div>
<ReactFlowProvider>
Expand Down
17 changes: 2 additions & 15 deletions apps/sim/app/workspace/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
isUpgradeReason,
UPGRADE_REASON_PARAM,
} from '@/lib/billing/upgrade-reasons'
import { ApplicationLoading } from '@/app/_shell/application-loading'
import { DesktopTitleBarLane } from '@/app/_shell/desktop-title-bar'
import { useWorkspacesWithMetadata } from '@/hooks/queries/workspace'

Expand Down Expand Up @@ -213,21 +214,7 @@ export default function WorkspacePage() {
)
}

return (
<div className='desktop-title-bar-page flex w-full items-center justify-center'>
<DesktopTitleBarLane />
<div
className='size-[18px] animate-spin rounded-full'
style={{
background:
'conic-gradient(from 0deg, hsl(var(--muted-foreground)) 0deg 120deg, transparent 120deg 180deg, hsl(var(--muted-foreground)) 180deg 300deg, transparent 300deg 360deg)',
mask: 'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
WebkitMask:
'radial-gradient(farthest-side, transparent calc(100% - 1.5px), black calc(100% - 1.5px))',
}}
/>
</div>
)
return <ApplicationLoading />
}

async function handleWorkflowRedirect(
Expand Down
16 changes: 10 additions & 6 deletions packages/emcn/src/components/sim-wordmark/sim-wordmark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@ const WORDMARK_FILLS = {
'muted-inverse': 'var(--text-muted-inverse)',
} as const

const WORDMARK_SIZES = {
nav: { width: 37, height: 18, className: '-translate-y-[1.5px] h-[18px] w-auto' },
tag: { width: 21, height: 10, className: 'h-[10px] w-auto' },
loading: { width: 67, height: 32, className: 'h-8 w-auto' },
} as const

export interface SimWordmarkProps {
/** Navbar mark or compact mark sized for a 20px ChipTag. */
size?: 'nav' | 'tag'
/** Navbar, compact 20px ChipTag, or centered application loading mark. */
size?: keyof typeof WORDMARK_SIZES
/** Body ink, inherited foreground, muted brand ink, or light ink for inverse surfaces. */
tone?: keyof typeof WORDMARK_FILLS
}

/** Canonical Sim logotype, shared by browser and bundled desktop pages. */
export function SimWordmark({ size = 'nav', tone = 'body' }: SimWordmarkProps) {
const compact = size === 'tag'
const width = compact ? 21 : 37
const height = compact ? 10 : 18
const { width, height, className } = WORDMARK_SIZES[size]
const fill = WORDMARK_FILLS[tone]

return (
Expand All @@ -28,7 +32,7 @@ export function SimWordmark({ size = 'nav', tone = 'body' }: SimWordmarkProps) {
height={height}
fill='none'
aria-hidden='true'
className={compact ? 'h-[10px] w-auto' : '-translate-y-[1.5px] h-[18px] w-auto'}
className={className}
>
<g fill={fill}>
{WORDMARK_PATHS.map((d) => (
Expand Down
3 changes: 0 additions & 3 deletions scripts/check-unused-exports.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -956,11 +956,8 @@
"apps/sim/ee/whitelabeling/branding.ts#ThemeColors",
"apps/sim/ee/whitelabeling/hooks/whitelabel.ts#WHITELABEL_STALE_TIME",
"apps/sim/ee/whitelabeling/hooks/whitelabel.ts#whitelabelKeys",
"apps/sim/ee/whitelabeling/index.ts#BrandConfig",
"apps/sim/ee/whitelabeling/index.ts#OrganizationWhitelabelSettings",
"apps/sim/ee/whitelabeling/index.ts#ThemeColors",
"apps/sim/ee/whitelabeling/index.ts#generateOrgThemeCSS",
"apps/sim/ee/whitelabeling/index.ts#mergeOrgBrandConfig",
"apps/sim/ee/workspace-forking/components/fork-resource-picker/fork-resource-picker.tsx#ForkResourcePickerItem",
"apps/sim/ee/workspace-forking/components/fork-sync/use-fork-sync.ts#MappableMappingKind",
"apps/sim/ee/workspace-forking/hooks/background-work.ts#BACKGROUND_WORK_STALE_TIME",
Expand Down
Loading