Skip to content

Commit 1f2c38d

Browse files
feat(dashboards): headerless dashboard page with a grid-matched icon
The dashboard page renders only the dashboard (or its empty state), so the header, its Delete action, and the delete route, hook, and use case go away. A new EMCN Dashboard icon is drawn on the shared sidebar icon grid and replaces the analytics ChartColumn on dashboard surfaces. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV
1 parent 3c4ef4f commit 1f2c38d

16 files changed

Lines changed: 145 additions & 266 deletions

File tree

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
import {
2-
deleteWorkspaceDashboardContract,
3-
readWorkspaceDashboardContract,
4-
} from '@/lib/api/contracts/dashboards'
1+
import { readWorkspaceDashboardContract } from '@/lib/api/contracts/dashboards'
52
import {
63
defineInternalJsonRoute,
74
internalOrchestrationErrorPolicy,
85
internalRateLimits,
96
internalSessionAuth,
107
} from '@/lib/api/server/routes'
11-
import {
12-
deleteWorkspaceDashboard,
13-
readWorkspaceDashboard,
14-
} from '@/lib/dashboards/application/dashboards'
8+
import { readWorkspaceDashboard } from '@/lib/dashboards/application/dashboards'
159
import { dashboardOperations } from '@/lib/dashboards/application/operations'
1610

1711
export const GET = defineInternalJsonRoute({
@@ -23,13 +17,3 @@ export const GET = defineInternalJsonRoute({
2317
mapInput: ({ params }) => ({ workspaceId: params.id }),
2418
useCase: readWorkspaceDashboard,
2519
})
26-
27-
export const DELETE = defineInternalJsonRoute({
28-
contract: deleteWorkspaceDashboardContract,
29-
auth: internalSessionAuth,
30-
operation: dashboardOperations.delete,
31-
rateLimit: internalRateLimits.user({ bucketName: 'dashboards' }),
32-
errorPolicy: internalOrchestrationErrorPolicy,
33-
mapInput: ({ params }) => ({ workspaceId: params.id }),
34-
useCase: deleteWorkspaceDashboard,
35-
})
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
'use client'
22

3-
import { ChartColumn } from '@sim/emcn/icons'
4-
import { ResourceChromeFallback } from '@/app/workspace/[workspaceId]/components/resource/components/resource-chrome-fallback'
3+
import { DashboardLoading } from '@/components/dashboards/dashboard-loading'
4+
import { Resource } from '@/app/workspace/[workspaceId]/components/resource/resource'
55

66
export default function DashboardsLoading() {
7-
return <ResourceChromeFallback icon={ChartColumn} title='Dashboard' />
7+
return (
8+
<Resource>
9+
<DashboardLoading />
10+
</Resource>
11+
)
812
}

‎apps/sim/app/workspace/[workspaceId]/home/components/chat-context-kind-registry/chat-context-kind-registry.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ReactNode } from 'react'
22
import {
3-
ChartColumn,
3+
Dashboard,
44
Database,
55
Folder as FolderIcon,
66
Globe,
@@ -82,7 +82,7 @@ export const CHAT_CONTEXT_KIND_REGISTRY: Record<ChatContextKind, ChatContextKind
8282
},
8383
dashboard: {
8484
label: 'Dashboard',
85-
renderIcon: ({ className }) => <ChartColumn className={className} />,
85+
renderIcon: ({ className }) => <Dashboard className={className} />,
8686
},
8787
file: {
8888
label: 'File',

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ export const ResourceContent = memo(function ResourceContent({
300300
)
301301

302302
case 'dashboard':
303-
return <DashboardResource key={resource.id} workspaceId={workspaceId} embedded />
303+
return <DashboardResource key={resource.id} workspaceId={workspaceId} />
304304
case 'file':
305305
return (
306306
<EmbeddedFile

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import type { ElementType, ReactNode } from 'react'
44
import { cn, OverflowText } from '@sim/emcn'
55
import {
6-
ChartColumn,
76
Connections,
7+
Dashboard,
88
Database,
99
File as FileIcon,
1010
Folder as FolderIcon,
@@ -187,11 +187,11 @@ export const RESOURCE_REGISTRY: Record<MothershipResourceType, ResourceTypeConfi
187187
dashboard: {
188188
type: 'dashboard',
189189
label: 'Dashboards',
190-
icon: ChartColumn,
190+
icon: Dashboard,
191191
renderTabIcon: (_resource, className) => (
192-
<ChartColumn className={cn(className, 'text-[var(--text-icon)]')} />
192+
<Dashboard className={cn(className, 'text-[var(--text-icon)]')} />
193193
),
194-
renderDropdownItem: (props) => <IconDropdownItem {...props} icon={ChartColumn} />,
194+
renderDropdownItem: (props) => <IconDropdownItem {...props} icon={Dashboard} />,
195195
},
196196
file: {
197197
type: 'file',

‎apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
} from '@sim/emcn'
2626
import {
2727
Building,
28-
ChartColumn,
28+
Dashboard,
2929
Database,
3030
Files,
3131
Integration,
@@ -727,7 +727,7 @@ export const Sidebar = memo(function Sidebar({ organizationHref }: SidebarProps)
727727
{
728728
id: 'dashboards',
729729
label: 'Dashboard',
730-
icon: ChartColumn,
730+
icon: Dashboard,
731731
href: `/workspace/${workspaceId}/dashboards`,
732732
hidden: !dashboardsEnabled || (permissionConfig.hideFilesTab && !accessRequestsEnabled),
733733
restricted: permissionConfig.hideFilesTab,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Loader } from '@sim/emcn/icons'
2+
3+
/** Kept apart from the dashboard renderer so the route's loading fallback stays light. */
4+
export function DashboardLoading() {
5+
return (
6+
<div role='status' className='flex flex-1 items-center justify-center'>
7+
<Loader animate className='size-[16px] text-[var(--text-icon)]' />
8+
<span className='sr-only'>Loading dashboard</span>
9+
</div>
10+
)
11+
}
Lines changed: 4 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
11
'use client'
22

3-
import { useState } from 'react'
4-
import {
5-
ChipModal,
6-
ChipModalBody,
7-
ChipModalError,
8-
ChipModalFooter,
9-
ChipModalHeader,
10-
} from '@sim/emcn'
11-
import { ChartColumn, Loader, Trash } from '@sim/emcn/icons'
123
import { DashboardFeatureGate } from '@/components/dashboards/dashboard-feature-gate'
4+
import { DashboardLoading } from '@/components/dashboards/dashboard-loading'
135
import { DashboardPreview } from '@/components/dashboards/dashboard-preview'
146
import { EmptyState } from '@/components/empty-state/empty-state'
157
import { Resource } from '@/app/workspace/[workspaceId]/components/resource/resource'
168
import { useWorkspaceFilesRoom } from '@/app/workspace/[workspaceId]/files/hooks/use-workspace-files-room'
17-
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
18-
import { useDeleteWorkspaceDashboard, useWorkspaceDashboard } from '@/hooks/queries/dashboards'
9+
import { useWorkspaceDashboard } from '@/hooks/queries/dashboards'
1910

2011
interface DashboardResourceProps {
2112
workspaceId: string
22-
/** The Chat resource panel supplies its own chrome; the page renders the header and actions. */
23-
embedded?: boolean
2413
}
2514

2615
/** The workspace's single dashboard, or an empty state until Sim saves the first one. */
@@ -32,38 +21,14 @@ export function DashboardResource(props: DashboardResourceProps) {
3221
)
3322
}
3423

35-
function EnabledDashboardResource({ workspaceId, embedded }: DashboardResourceProps) {
24+
function EnabledDashboardResource({ workspaceId }: DashboardResourceProps) {
3625
useWorkspaceFilesRoom(workspaceId)
37-
const { canEdit } = useUserPermissionsContext()
3826
const query = useWorkspaceDashboard(workspaceId)
39-
const [confirmingDelete, setConfirmingDelete] = useState(false)
4027
const dashboard = query.data?.dashboard ?? null
4128
return (
4229
<Resource>
43-
{!embedded && (
44-
<Resource.Header
45-
icon={ChartColumn}
46-
title='Dashboard'
47-
actions={
48-
dashboard && canEdit
49-
? [
50-
{
51-
id: 'delete',
52-
icon: Trash,
53-
text: 'Delete',
54-
variant: 'destructive',
55-
onSelect: () => setConfirmingDelete(true),
56-
},
57-
]
58-
: []
59-
}
60-
/>
61-
)}
6230
{query.isPending ? (
63-
<div role='status' className='flex flex-1 items-center justify-center'>
64-
<Loader animate className='size-[16px] text-[var(--text-icon)]' />
65-
<span className='sr-only'>Loading dashboard</span>
66-
</div>
31+
<DashboardLoading />
6732
) : query.error ? (
6833
<div className='p-6 text-[var(--text-error)]' role='alert'>
6934
{query.error.message}
@@ -82,47 +47,6 @@ function EnabledDashboardResource({ workspaceId, embedded }: DashboardResourcePr
8247
description='Sim will build your dashboard here once your workspace is set up.'
8348
/>
8449
)}
85-
{confirmingDelete && (
86-
<DeleteDashboardModal
87-
workspaceId={workspaceId}
88-
onClose={() => setConfirmingDelete(false)}
89-
/>
90-
)}
9150
</Resource>
9251
)
9352
}
94-
95-
interface DeleteDashboardModalProps {
96-
workspaceId: string
97-
onClose: () => void
98-
}
99-
100-
function DeleteDashboardModal({ workspaceId, onClose }: DeleteDashboardModalProps) {
101-
const remove = useDeleteWorkspaceDashboard(workspaceId)
102-
return (
103-
<ChipModal
104-
size='sm'
105-
open
106-
onOpenChange={(open) => {
107-
if (!open) onClose()
108-
}}
109-
srTitle='Delete dashboard'
110-
>
111-
<ChipModalHeader onClose={onClose}>Delete dashboard</ChipModalHeader>
112-
<ChipModalBody>
113-
<p className='px-2 text-[var(--text-secondary)]'>Delete this workspace’s dashboard?</p>
114-
{remove.error && <ChipModalError>{remove.error.message}</ChipModalError>}
115-
</ChipModalBody>
116-
<ChipModalFooter
117-
defaultAction='dismiss'
118-
onCancel={onClose}
119-
primaryAction={{
120-
label: 'Delete',
121-
variant: 'destructive',
122-
disabled: remove.isPending,
123-
onClick: () => remove.mutate(undefined, { onSuccess: onClose }),
124-
}}
125-
/>
126-
</ChipModal>
127-
)
128-
}
Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
1+
import { useQuery } from '@tanstack/react-query'
22
import { requestJson } from '@/lib/api/client/request'
3-
import {
4-
deleteWorkspaceDashboardContract,
5-
readWorkspaceDashboardContract,
6-
} from '@/lib/api/contracts/dashboards'
7-
import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
3+
import { readWorkspaceDashboardContract } from '@/lib/api/contracts/dashboards'
84

95
export const DASHBOARD_STALE_TIME = 30_000
106
export const dashboardKeys = {
@@ -22,15 +18,3 @@ export function useWorkspaceDashboard(workspaceId: string, options?: { enabled?:
2218
staleTime: DASHBOARD_STALE_TIME,
2319
})
2420
}
25-
26-
export function useDeleteWorkspaceDashboard(workspaceId: string) {
27-
const client = useQueryClient()
28-
return useMutation({
29-
mutationFn: () =>
30-
requestJson(deleteWorkspaceDashboardContract, { params: { id: workspaceId } }),
31-
onSettled: () => {
32-
void client.invalidateQueries({ queryKey: dashboardKeys.workspace(workspaceId) })
33-
void client.invalidateQueries({ queryKey: workspaceFilesKeys.lists() })
34-
},
35-
})
36-
}

‎apps/sim/lib/api/contracts/dashboards.ts‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,4 @@ export const readWorkspaceDashboardContract = defineRouteContract({
2929
}),
3030
},
3131
})
32-
export const deleteWorkspaceDashboardContract = defineRouteContract({
33-
method: 'DELETE',
34-
path: '/api/workspaces/[id]/dashboard',
35-
params: workspaceParams,
36-
response: { mode: 'json', schema: z.object({ deleted: z.literal(true), id: z.string() }) },
37-
})
3832
export type DashboardRecord = z.output<typeof dashboardRecordSchema>

0 commit comments

Comments
 (0)