Skip to content

Commit be50bc6

Browse files
feat(dashboards): Sim-built workspace dashboard behind a rollout flag (#8305)
* feat(dashboards): add table-backed dashboard resources behind rollout flag * refactor(files): separate discovery from storage context * fix(charts): keep pie labels readable and separate neutral colors * feat(dashboards): compute percentages from row conditions * refactor(dashboards): store dashboards as workspace files Dashboards are now ordinary workspace files, handled like Sim pages, instead of a separate resource. Creating or uploading `<Name>.dashboard` drops the suffix and stamps `text/x-sim-dashboard`; the type is sticky across content writes. The file viewer renders it live behind the `dashboards` flag, and the public share viewer shows a workspace-only notice. - Remove the dashboard resource: sidebar page, API routes, hooks, contracts, application layer, Mothership dashboards/dashboard_folders tools, resource tags, and the per-turn dashboardsEnabled payload. - Revert the file discovery column (0385) and drop the dashboard folder resource enum (0384); dashboards never shipped, so no backfill. - Chat panel decides previewability and tab/picker icons from the file type, not the name, so extensionless dashboards render and get the chart icon. - Renderer: authored left label columns are kept intact, horizontal bar frames grow with row count, and hovered rows get a label-and-bar highlight. - Simplify the create-dashboard skill around one validated example. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * fix(dashboards): address review findings on chart layout and precision - Size horizontal bar `.chart` previews by category count like dashboard panels. - Keep the ECharts label column for percentage bar widths, resolve percentage grid insets for the row highlight, and keep the time axis on the queried range. - Show small readout values with significant digits instead of rounding to 0. - Pass the dashboard's timezone-adjusted today to the range calendar. - Decide the Chat panel's Markdown mode from the file record. - Replace mock-call assertions in the EChartsView tests with DOM behavior. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * fix(charts): size grouped bar rows and keep the row highlight through resizes Unstacked bar series sit side by side in a category row, so grouped charts keep the ECharts label column and their rows fit every bar slot. The row highlight redraws the active row after each render, so a resize moves it with the plot. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * fix(charts): keep authored tooltip arrays and drop the v2 chat mode override Chart tooltip defaults now apply to every entry of an authored `tooltip` array instead of replacing it with one object. Restore the v2 chat payload to staging: the hard-coded `mode: 'agent'` came from the removed dashboard resource work and would overwrite a resumed chat's saved mode. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * fix(charts): reserve the authored bar gap between grouped bars Grouped horizontal bar rows now size their gap from the series `barGap` the way ECharts does (pixels, a percentage of bar width, default 20%, overlap for negative gaps) instead of a fixed 4px. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * fix(charts): format index-encoded pie tooltips and test dashboard refresh through the UI Pie tooltips encoded by dimension index now use the custom formatter, resolving the measure through the encode indexes ECharts passes it. The dashboard preview test drops the mocked controls and callback-driven cases and clicks the real Refresh button to check which analytics queries are invalidated; the range and timezone math stays covered by the time tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * fix(charts): format bar tooltip values with each series' own value axis Multi-axis bar charts now read the tooltip unit from the axis each series is plotted on, so a secondary axis no longer inherits the first axis's format. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * test(dashboards): drop mock-call and rendered-text assertions Removes assertions on mocked collaborators and rendered text from the dashboard, chart and analytics tests per the repository testing rules, keeping the observable checks (status codes, thrown errors, DOM roles, computed summaries, emitted zoom ranges). Deletes the animated-number test, which had no behavior left to assert. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * test(dashboards): drop redundant mock resets and default environment pragmas The shared Vitest config already resets mocks and stubbed globals before each test and defaults to the node environment. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * refactor(dashboards): move authoring guidance to Mothership like Sim Pages Drop the create-dashboard built-in skill and its rollout gating in the skill lists. Mothership now learns the dashboard format from a sim-dashboards reference in its own research-and-deliverables skill, the pattern Sim Pages use; Sim workspace built-ins stay Agent-block documents. The dashboards flag still gates the viewer and table analytics. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * refactor(dashboards): give DashboardFeatureGate a props interface Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * feat(dashboards): report dashboard parse errors on file writes The v2 file create, replace and edit responses now carry `diagnostics` for a dashboard file: its parse errors, or an empty list. Writes are never blocked, like the page lint; table columns and queries are still checked when panels render. Parse errors are reported as `path: message` lines, and a block with no recognized kind names the allowed kinds and unknown keys instead of Zod's union dump. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * fix(dashboards): split diagnostics, switch panel query modes, return create revision - One diagnostics entry per parse error instead of a newline-joined string. - A panel choosing aggregate or columns drops the other mode's inherited dashboard fields, so shared defaults serve table and aggregate panels. - The v2 create response returns the revision it produced, like the replace and edit responses. - The analytics route test uses the shared createMockRequest helper. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * fix(dashboards): drop inherited ordering on query-mode switches and cap diagnostics size A panel switching away from the dashboard's query mode no longer inherits its sort or limit, which name aggregate aliases or top-N groups in one mode and rows in the other. Write diagnostics check the 128 KB source limit before decoding the payload. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * revert(dashboards): restore dashboards as a separate workspace resource Reverts "store dashboards as workspace files" and "move authoring guidance to Mothership like Sim Pages", restoring the dashboard resource: its page, folders, APIs, resource tags, Mothership dashboards/dashboard_folders tools, per-turn availability, the create-dashboard built-in skill, and file discovery. Removes the write-time diagnostics that only applied to plain files. Keeps the chart renderer fixes, readable parse errors, panel query-mode handling, the create-response revision, and the test cleanups. The authoring skill keeps its simplified form with the default-formatting note. Migrations are regenerated on top of staging in the next commit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * fix(dashboards): renumber resource migrations after staging and use central test mocks Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * fix(dashboards): list by folder, canonical folder paths, named props The dashboard browser now asks for one folder's dashboards instead of filtering the first 500 across the workspace, folder paths use the shared segment encoder, and the README names the renumbered migrations. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * fix(dashboards): address review round on the restored resource Fork payloads accept file discovery, dashboard queries are keyed and invalidated per workspace, dashboard contexts open tabs and resolve in organization chats, archived dashboard folders keep their paths, and the folder dialog skips no-op moves and hides the edited subtree. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * fix(dashboards): cap dashboard folders and reject folder name collisions Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * fix(dashboards): name the dashboards page props Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * feat(mothership): send entitlements instead of dashboardsEnabled Restores the entitlement evaluator registry removed in v1.0.0 and sends the granted list with every Mothership turn; dashboards is the first entitlement. Sim still enforces every gated operation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * refactor(dashboards): give agent dashboard commands the files folder syntax Dashboards take folder paths like files: create/list --folder, move --to (/ is the root), rename and set-content as separate actions, and folder create/move/delete by positional path with --recursive for non-empty deletes. The UI routes keep folder ids. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * feat(dashboards): one Sim-built dashboard per workspace A workspace now has a single dashboard that Sim builds. The Dashboards page renders it directly, or an empty state until the first save. Folders, the list, create, rename and move are removed; the agent reads and saves it with dashboards get / set, and replacing existing content needs its revision. A partial unique index enforces one live dashboard per workspace, and generic file writes can no longer produce the dashboard content type. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * feat(dashboards): put Dashboard under New chat in the sidebar Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV * 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 * refactor(dashboards): drop file discovery for a content-type exclusion With one dashboard per workspace, the listed/unlisted discovery column, its migration, legacy-writer trigger, and the upload backfill script are no longer needed. File listings, search, and workflow file pickers exclude the dashboard content type instead, and the one-dashboard index is now migration 0392. * refactor(dashboards): store dashboards in their own table A dashboard is now a row in a dashboard table with its own id, keyed to its workspace by a unique index (dropping it later allows several). Reads and saves go through a small repository with a revision check, so the file-side guards, exclusions, fixed file name, and dashboard file previews are gone. Chat contexts reference dashboards by dashboardId, and saves are audited as dashboard events. * fix(dashboards): accept dashboard chat contexts and bound revisions The chat request schema now admits dashboard contexts, revisions outside the integer column range are rejected as validation errors, resource tabs only fetch the dashboard when one is open, and the authoring guide notes that range-pinned panels do not drag-zoom. * fix(dashboards): named heading sizes and no redundant dashboard name lookups The dashboard name is fixed, so resource tabs and chat chips no longer fetch the dashboard to title it; dashboard headings use named text sizes. * refactor(dashboards): keep the authoring reference in Mothership's skill Mothership's create-dashboard skill now carries the dashboard syntax like every other worker skill, so the builtin-create-dashboard workspace skill, its source, and its flag gating are removed. * fix(dashboards): server loading fallback and muted gate text * chore(db): drop leftover script-migration test fixture change --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 594894c commit be50bc6

131 files changed

Lines changed: 36223 additions & 362 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/docs/openapi-v2-files-audit.json‎

Lines changed: 129 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@
248248
"content": {
249249
"application/json": {
250250
"schema": {
251-
"$ref": "#/components/schemas/V2FileResponse"
251+
"$ref": "#/components/schemas/V2CreatedFileResponse"
252252
}
253253
}
254254
}
@@ -4044,18 +4044,112 @@
40444044
}
40454045
]
40464046
},
4047-
"V2FileResponse": {
4047+
"V2CreatedFile": {
4048+
"type": "object",
4049+
"properties": {
4050+
"id": {
4051+
"type": "string",
4052+
"description": "Unique file identifier.",
4053+
"examples": ["wf_V1StGXR8z5jdHi6BmyT91"]
4054+
},
4055+
"webUrl": {
4056+
"type": "string",
4057+
"format": "uri",
4058+
"description": "Canonical absolute URL for opening this resource in the Sim web application."
4059+
},
4060+
"name": {
4061+
"type": "string",
4062+
"description": "Original file name.",
4063+
"examples": ["data.csv"]
4064+
},
4065+
"size": {
4066+
"type": "number",
4067+
"minimum": 0,
4068+
"description": "Size in bytes of the stored file. For a generated document (docx, pptx, pdf, xlsx) this is the generation source, not the rendered document, so it does not predict how many bytes downloading the file returns.",
4069+
"examples": [1024]
4070+
},
4071+
"type": {
4072+
"type": "string",
4073+
"description": "MIME type of the stored file. For a generated document (docx, pptx, pdf, xlsx) this is the generation source type, not the rendered document type a download serves.",
4074+
"examples": ["text/csv"]
4075+
},
4076+
"key": {
4077+
"type": "string",
4078+
"description": "Storage key for the file.",
4079+
"examples": ["workspace/example/data.csv"]
4080+
},
4081+
"folderPath": {
4082+
"type": "string",
4083+
"title": "Folder path",
4084+
"description": "Canonical containing-folder path. `/` is the workspace root.",
4085+
"maxLength": 4096
4086+
},
4087+
"uploadedByEmail": {
4088+
"type": "string",
4089+
"format": "email",
4090+
"pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$",
4091+
"description": "Current email address of the uploader.",
4092+
"examples": ["jane@example.com"]
4093+
},
4094+
"uploadedAt": {
4095+
"type": "string",
4096+
"description": "ISO 8601 timestamp when the file was uploaded.",
4097+
"format": "date-time",
4098+
"examples": ["2026-01-15T10:30:00Z"]
4099+
},
4100+
"updatedAt": {
4101+
"type": "string",
4102+
"description": "ISO 8601 timestamp of the last content or metadata write.",
4103+
"format": "date-time",
4104+
"examples": ["2026-01-15T10:30:00Z"]
4105+
},
4106+
"deletedAt": {
4107+
"anyOf": [
4108+
{
4109+
"type": "string"
4110+
},
4111+
{
4112+
"type": "null"
4113+
}
4114+
],
4115+
"description": "ISO 8601 timestamp when the file was archived by deleting it, or null while the file is active. Only an archived-scope file list returns files with a non-null value.",
4116+
"format": "date-time",
4117+
"examples": ["2026-01-16T09:00:00Z"]
4118+
},
4119+
"revision": {
4120+
"description": "Opaque token for the content this write produced. Send it back as `expectedRevision` on the next write. Absent for a file with no recorded content version.",
4121+
"type": "string"
4122+
}
4123+
},
4124+
"required": [
4125+
"id",
4126+
"webUrl",
4127+
"name",
4128+
"size",
4129+
"type",
4130+
"key",
4131+
"folderPath",
4132+
"uploadedByEmail",
4133+
"uploadedAt",
4134+
"updatedAt",
4135+
"deletedAt"
4136+
],
4137+
"additionalProperties": false,
4138+
"title": "Created file",
4139+
"description": "A newly created workspace file, with the revision it produced."
4140+
},
4141+
"V2CreatedFileResponse": {
40484142
"type": "object",
40494143
"properties": {
40504144
"data": {
40514145
"description": "Response data.",
4052-
"$ref": "#/components/schemas/V2File"
4146+
"$ref": "#/components/schemas/V2CreatedFile"
40534147
}
40544148
},
40554149
"required": ["data"],
40564150
"additionalProperties": false,
4057-
"title": "File response",
4058-
"description": "A single workspace file.",
4151+
"title": "Created file response",
4152+
"description": "A newly created workspace file, with the revision it produced.",
40594153
"examples": [
40604154
{
40614155
"data": {
@@ -5115,6 +5209,36 @@
51155209
"title": "Delete file response",
51165210
"description": "Deletion confirmation for one file."
51175211
},
5212+
"V2FileResponse": {
5213+
"type": "object",
5214+
"properties": {
5215+
"data": {
5216+
"description": "Response data.",
5217+
"$ref": "#/components/schemas/V2File"
5218+
}
5219+
},
5220+
"required": ["data"],
5221+
"additionalProperties": false,
5222+
"title": "File response",
5223+
"description": "A single workspace file.",
5224+
"examples": [
5225+
{
5226+
"data": {
5227+
"id": "wf_V1StGXR8z5jdHi6BmyT91",
5228+
"webUrl": "https://www.sim.ai/workspace/a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64/files/wf_V1StGXR8z5jdHi6BmyT91",
5229+
"name": "data.csv",
5230+
"size": 1024,
5231+
"type": "text/csv",
5232+
"key": "workspace/example/data.csv",
5233+
"folderPath": "/Engineering",
5234+
"uploadedByEmail": "jane@example.com",
5235+
"uploadedAt": "2026-01-15T10:30:00Z",
5236+
"updatedAt": "2026-01-15T10:30:00Z",
5237+
"deletedAt": null
5238+
}
5239+
}
5240+
]
5241+
},
51185242
"RenameFileRequest": {
51195243
"type": "object",
51205244
"properties": {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { authMockFns } from '@sim/testing/mocks/auth.mock'
2+
import { rateLimiterMock, rateLimiterMockFns } from '@sim/testing/mocks/rate-limiter.mock'
3+
import { createMockRequest } from '@sim/testing/mocks/request.mock'
4+
import { beforeEach, describe, expect, it, vi } from 'vitest'
5+
import { POST } from '@/app/api/table/[tableId]/analytics/route'
6+
7+
const hoisted = vi.hoisted(() => ({ execute: vi.fn() }))
8+
vi.mock('@/lib/core/rate-limiter', () => rateLimiterMock)
9+
vi.mock('@/lib/table/application/analytics', () => ({
10+
readTableAnalytics: { operation: { id: 'tables.rows.analytics' }, execute: hoisted.execute },
11+
}))
12+
const mocks = {
13+
...hoisted,
14+
session: authMockFns.mockGetSession,
15+
limit: rateLimiterMockFns.mockEnforceUserRateLimit,
16+
}
17+
const context = { params: Promise.resolve({ tableId: 'tbl_test' }) }
18+
const body = {
19+
workspaceId: 'workspace_test',
20+
query: {
21+
from: '2026-09-01T00:00:00Z',
22+
to: '2026-09-02T00:00:00Z',
23+
aggregate: { n: { op: 'count' } },
24+
},
25+
}
26+
const request = (value: unknown) =>
27+
createMockRequest({ method: 'POST', url: '/api/table/tbl_test/analytics', body: value })
28+
beforeEach(() => {
29+
mocks.session.mockResolvedValue({ user: { id: 'viewer' }, session: { id: 'session' } })
30+
mocks.limit.mockResolvedValue(null)
31+
mocks.execute.mockResolvedValue({
32+
rows: [{ n: 0 }],
33+
columns: ['n'],
34+
columnLabels: { n: 'n' },
35+
truncated: false,
36+
bucket: null,
37+
})
38+
})
39+
describe('analytics HTTP adapter', () => {
40+
it('authenticates before parsing and never uses a file share as authority', async () => {
41+
mocks.session.mockResolvedValue(null)
42+
expect((await POST(request({ invalid: true }), context)).status).toBe(401)
43+
})
44+
it('validates the contract before the use case', async () => {
45+
expect(
46+
(await POST(request({ ...body, query: { ...body.query, sql: 'select *' } }), context)).status
47+
).toBe(400)
48+
})
49+
it('emits a private response', async () => {
50+
const response = await POST(request(body), context)
51+
expect(response.status).toBe(200)
52+
expect(response.headers.get('cache-control')).toBe('private, no-store')
53+
expect(await response.json()).toMatchObject({ rows: [{ n: 0 }], truncated: false })
54+
})
55+
})
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { queryTableAnalyticsContract } from '@/lib/api/contracts/table-analytics'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { readTableAnalytics } from '@/lib/table/application/analytics'
9+
import { tableOperations } from '@/lib/table/application/operations'
10+
11+
export const POST = defineInternalJsonRoute({
12+
contract: queryTableAnalyticsContract,
13+
auth: internalSessionAuth,
14+
operation: tableOperations.analytics,
15+
rateLimit: internalRateLimits.user({
16+
bucketName: 'table-analytics',
17+
config: { maxTokens: 120, refillRate: 60, refillIntervalMs: 60_000 },
18+
}),
19+
errorPolicy: internalOrchestrationErrorPolicy,
20+
parseOptions: { maxBodyBytes: 64 * 1024 },
21+
mapInput: ({ params, body }) => ({
22+
tableId: params.tableId,
23+
assertedWorkspaceId: body.workspaceId,
24+
query: body.query,
25+
}),
26+
useCase: readTableAnalytics,
27+
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
28+
})

‎apps/sim/app/api/v2/files/[fileId]/content/route.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ export const PUT = defineV2JsonRoute({
4141
}),
4242
useCase: updateWorkspaceFileContent,
4343
present: async ({ file }) => ({
44-
data: { ...(await toV2File(file)), ...workspaceFileRevisionField(file) },
44+
data: {
45+
...(await toV2File(file)),
46+
...workspaceFileRevisionField(file),
47+
},
4548
}),
4649
})
4750

@@ -78,6 +81,10 @@ export const PATCH = defineV2JsonRoute({
7881
}),
7982
useCase: editWorkspaceFileContent,
8083
present: async ({ file, lineCount }) => ({
81-
data: { file: await toV2File(file), lineCount, ...workspaceFileRevisionField(file) },
84+
data: {
85+
file: await toV2File(file),
86+
lineCount,
87+
...workspaceFileRevisionField(file),
88+
},
8289
}),
8390
})

‎apps/sim/app/api/v2/files/route.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/
99
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
1010
import { v2FileErrorPolicies } from '@/lib/workspace-files/api'
1111
import { createWorkspaceFile } from '@/lib/workspace-files/application/create-workspace-file'
12+
import { workspaceFileRevisionField } from '@/lib/workspace-files/application/file-revision'
1213
import { queryWorkspaceFilePage } from '@/lib/workspace-files/application/list-workspace-files'
1314
import { fileOperations } from '@/lib/workspace-files/application/operations'
1415
import { MAX_WORKSPACE_FILE_INLINE_BODY_BYTES } from '@/lib/workspace-files/orchestration'
@@ -93,5 +94,10 @@ export const POST = defineV2JsonRoute({
9394
exactName: true,
9495
}),
9596
useCase: createWorkspaceFile,
96-
present: async ({ file }) => ({ data: await toV2File(file) }),
97+
present: async ({ file }) => ({
98+
data: {
99+
...(await toV2File(file)),
100+
...workspaceFileRevisionField(file),
101+
},
102+
}),
97103
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { readWorkspaceDashboardContract } from '@/lib/api/contracts/dashboards'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { readWorkspaceDashboard } from '@/lib/dashboards/application/dashboards'
9+
import { dashboardOperations } from '@/lib/dashboards/application/operations'
10+
11+
export const GET = defineInternalJsonRoute({
12+
contract: readWorkspaceDashboardContract,
13+
auth: internalSessionAuth,
14+
operation: dashboardOperations.read,
15+
rateLimit: internalRateLimits.user({ bucketName: 'dashboards' }),
16+
errorPolicy: internalOrchestrationErrorPolicy,
17+
mapInput: ({ params }) => ({ workspaceId: params.id }),
18+
useCase: readWorkspaceDashboard,
19+
})

‎apps/sim/app/o/[organizationId]/home/components/composer/composer.test.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ async function render(
234234
<QueryClientProvider client={queryClient}>
235235
<FeatureFlagsProvider
236236
flags={{
237+
dashboards: false,
237238
'table-row-ttl': false,
238239
'mothership-model-selector': mocks.advanced,
239240
'mothership-plan-mode': mocks.plan,
@@ -303,6 +304,7 @@ it('keeps restored queued skills scoped when replacing a draft', async () => {
303304
<QueryClientProvider client={queryClient}>
304305
<FeatureFlagsProvider
305306
flags={{
307+
dashboards: false,
306308
'table-row-ttl': false,
307309
'mothership-model-selector': mocks.advanced,
308310
'mothership-plan-mode': mocks.plan,

‎apps/sim/app/o/[organizationId]/layout.tsx‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { redirect } from 'next/navigation'
44
import { SettingsNavigationProvider } from '@/components/settings/settings-navigation-provider'
55
import { getSession } from '@/lib/auth'
66
import { getActiveOrganizationId } from '@/lib/auth/session-response'
7+
import { isDashboardsEnabled } from '@/lib/dashboards/feature-flag'
78
import { isMothershipModelSelectorEnabled, isPlanModeEnabled } from '@/lib/mothership/feature-flags'
89
import { organizationRoutes, WORKSPACE_SETTINGS_PATH } from '@/lib/navigation/paths'
910
import { getOrganizationSurfaceContext } from '@/lib/organizations/surface'
@@ -55,23 +56,26 @@ export default async function OrganizationLayout({
5556
if (!context.mothershipAvailable && !context.searchAccess.memberScoped)
5657
redirect(WORKSPACE_SETTINGS_PATH)
5758

58-
const [, tableRowTtlEnabled, modelSelectorEnabled, planModeEnabled] = await Promise.all([
59-
prefetchOrganizationSidebar(
60-
queryClient,
61-
organizationId,
62-
{ kind: 'session', userId: session.user.id, sessionId: session.session.id },
63-
getActiveOrganizationId(session)
64-
),
65-
isTableRowTtlEnabled(),
66-
isMothershipModelSelectorEnabled(),
67-
isPlanModeEnabled(),
68-
])
59+
const [, tableRowTtlEnabled, modelSelectorEnabled, planModeEnabled, dashboardsEnabled] =
60+
await Promise.all([
61+
prefetchOrganizationSidebar(
62+
queryClient,
63+
organizationId,
64+
{ kind: 'session', userId: session.user.id, sessionId: session.session.id },
65+
getActiveOrganizationId(session)
66+
),
67+
isTableRowTtlEnabled(),
68+
isMothershipModelSelectorEnabled(),
69+
isPlanModeEnabled(),
70+
isDashboardsEnabled(organizationId),
71+
])
6972
const initialSidebarCollapsed = cookieStore.get('sidebar_collapsed')?.value === '1'
7073

7174
return (
7275
<HydrationBoundary state={dehydrate(queryClient)}>
7376
<FeatureFlagsProvider
7477
flags={{
78+
dashboards: dashboardsEnabled,
7579
'table-row-ttl': tableRowTtlEnabled,
7680
'mothership-model-selector': modelSelectorEnabled,
7781
'mothership-plan-mode': planModeEnabled,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { ReactNode } from 'react'
2+
import { DashboardFeatureGate } from '@/components/dashboards/dashboard-feature-gate'
3+
4+
interface DashboardLayoutProps {
5+
children: ReactNode
6+
}
7+
8+
export default function DashboardLayout({ children }: DashboardLayoutProps) {
9+
return <DashboardFeatureGate>{children}</DashboardFeatureGate>
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { DashboardLoading } from '@/components/dashboards/dashboard-loading'
2+
import { Resource } from '@/app/workspace/[workspaceId]/components/resource/resource'
3+
4+
export default function DashboardsLoading() {
5+
return (
6+
<Resource>
7+
<DashboardLoading />
8+
</Resource>
9+
)
10+
}

0 commit comments

Comments
 (0)