Skip to content

Commit 68e8448

Browse files
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
1 parent 5d0e7e4 commit 68e8448

3 files changed

Lines changed: 48 additions & 9 deletions

File tree

‎apps/sim/lib/dashboards/diagnostics.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { DASHBOARD_CONTENT_TYPE } from '@/lib/dashboards/file'
2-
import { parseDashboardSpec } from '@/lib/dashboards/spec'
2+
import {
3+
DASHBOARD_SOURCE_TOO_LARGE,
4+
MAX_DASHBOARD_SOURCE_BYTES,
5+
parseDashboardSpec,
6+
} from '@/lib/dashboards/spec'
37

48
/**
59
* Parse errors for a dashboard file's content, reported on write without blocking it, like
@@ -11,6 +15,7 @@ export function dashboardDiagnostics(
1115
content: Buffer | string
1216
): string[] | undefined {
1317
if (contentType !== DASHBOARD_CONTENT_TYPE) return undefined
18+
if (Buffer.byteLength(content) > MAX_DASHBOARD_SOURCE_BYTES) return [DASHBOARD_SOURCE_TOO_LARGE]
1419
const { error } = parseDashboardSpec(content.toString())
1520
return error ? error.split('\n') : []
1621
}

‎apps/sim/lib/dashboards/spec.test.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from 'vitest'
2-
import { parseDashboardSpec } from '@/lib/dashboards/spec'
2+
import { parseDashboardSpec, resolveDashboardSource } from '@/lib/dashboards/spec'
33
import {
44
dashboardRangeFromCalendar,
55
parseDashboardCustomRange,
@@ -124,6 +124,22 @@ describe('panel query modes', () => {
124124
})
125125
})
126126

127+
describe('inherited ordering across query modes', () => {
128+
it('does not carry a sort or limit into a panel that switches query mode', () => {
129+
const resolved = resolveDashboardSource(
130+
{
131+
tableId: 'tbl_1',
132+
groupBy: ['status'],
133+
aggregate: { n: { op: 'count' } },
134+
sort: [{ field: 'n', direction: 'desc' }],
135+
limit: 5,
136+
},
137+
{ columns: ['status'] }
138+
)
139+
expect(resolved).toEqual({ tableId: 'tbl_1', columns: ['status'] })
140+
})
141+
})
142+
127143
describe('dashboard parse errors', () => {
128144
it('names the block path, the allowed block kinds and any unknown key', () => {
129145
expect(parseDashboardSpec('title: Probe\nblocks:\n - bogus: 1\n').error).toBe(

‎apps/sim/lib/dashboards/spec.ts‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import {
1010
analyticsSelectionSchema,
1111
} from '@/lib/table/analytics/schema'
1212

13+
/** Dashboard YAML is bounded before parsing; writes report the same limit without decoding. */
14+
export const MAX_DASHBOARD_SOURCE_BYTES = 128 * 1024
15+
export const DASHBOARD_SOURCE_TOO_LARGE = 'Dashboard source exceeds 128 KB'
16+
1317
export const DASHBOARD_RANGES = ['1h', '24h', '7d', '30d', '90d'] as const
1418
export type DashboardRange = (typeof DASHBOARD_RANGES)[number]
1519
const rangeSchema = z.enum(DASHBOARD_RANGES)
@@ -89,19 +93,33 @@ const dashboardSchema: z.ZodType<DashboardSpec> = z
8993
})
9094
.strict()
9195

96+
function queryMode(source: DashboardSource | undefined): 'columns' | 'aggregate' | undefined {
97+
return source?.columns ? 'columns' : source?.aggregate ? 'aggregate' : undefined
98+
}
99+
92100
/**
93101
* A panel's source shallowly overrides the dashboard's. Choosing a query mode (`aggregate` or
94102
* `columns`) drops the other mode's inherited fields, so shared defaults serve both panel kinds.
103+
* Switching away from the dashboard's mode also drops its `sort` and `limit`, which name
104+
* aggregate aliases or top-N groups in one mode and rows in the other.
95105
*/
96106
export function resolveDashboardSource(
97107
defaults: DashboardSource | undefined,
98108
source: DashboardSource | undefined
99109
): ResolvedDashboardSource {
100-
const inherited = source?.columns
101-
? omit(defaults ?? {}, ['aggregate', 'groupBy', 'bucket'])
102-
: source?.aggregate
103-
? omit(defaults ?? {}, ['columns'])
104-
: defaults
110+
const mode = queryMode(source)
111+
const switched =
112+
mode !== undefined && queryMode(defaults) !== undefined && mode !== queryMode(defaults)
113+
const modeFields =
114+
mode === 'columns'
115+
? (['aggregate', 'groupBy', 'bucket'] as const)
116+
: mode === 'aggregate'
117+
? (['columns'] as const)
118+
: []
119+
const inherited = omit(defaults ?? {}, [
120+
...modeFields,
121+
...(switched ? (['sort', 'limit'] as const) : []),
122+
])
105123
const merged = { ...inherited, ...source }
106124
if (!merged.tableId)
107125
throw new Error('A data panel requires source.tableId, on the dashboard or on the panel')
@@ -163,8 +181,8 @@ export function parseDashboardSpec(
163181
content: string
164182
): { spec: DashboardSpec; error?: never } | { error: string; spec?: never } {
165183
try {
166-
if (new TextEncoder().encode(content).byteLength > 128 * 1024)
167-
throw new Error('Dashboard source exceeds 128 KB')
184+
if (new TextEncoder().encode(content).byteLength > MAX_DASHBOARD_SOURCE_BYTES)
185+
throw new Error(DASHBOARD_SOURCE_TOO_LARGE)
168186
const raw: unknown = load(content, { schema: JSON_SCHEMA })
169187
const measured = measureYamlExpansion(raw, {
170188
maxNodes: 10000,

0 commit comments

Comments
 (0)