Skip to content

Commit b66bb28

Browse files
authored
Merge pull request #290 from dekart-xyz/snapshot-viewport-params
Add viewport parameters support for report snapshots
2 parents e972058 + 4dd16b5 commit b66bb28

13 files changed

Lines changed: 2088 additions & 1386 deletions

File tree

Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ proto-clean:
3131
rm -rf ./node_modules/dekart-proto
3232

3333
proto-build: proto-clean #to run inside docker
34-
protoc --proto_path=./proto --js_out=import_style=commonjs,binary:./proto $$(find proto -type f -name "*.proto")
35-
protoc --proto_path=./proto --ts_out=service=grpc-web:./proto $$(find proto -type f -name "*.proto")
36-
protoc --go_out=./src $$(find proto -type f -name "*.proto")
37-
protoc --go-grpc_out=./src $$(find proto -type f -name "*.proto")
34+
protoc --experimental_allow_proto3_optional --proto_path=./proto --js_out=import_style=commonjs,binary:./proto $$(find proto -type f -name "*.proto")
35+
protoc --experimental_allow_proto3_optional --proto_path=./proto --ts_out=service=grpc-web:./proto $$(find proto -type f -name "*.proto")
36+
protoc --experimental_allow_proto3_optional --go_out=./src $$(find proto -type f -name "*.proto")
37+
protoc --experimental_allow_proto3_optional --go-grpc_out=./src $$(find proto -type f -name "*.proto")
3838

3939
proto-docker: # build docker container for building protos
4040
ifeq ($(UNAME),arm64)
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/* eslint-disable no-undef */
2+
3+
const appUrl = Cypress.env('DEKART_E2E_BASE_URL') || 'http://localhost:3000'
4+
const ciValue = String(Cypress.env('CI') ?? '').toLowerCase()
5+
const isCI = ciValue === 'true' || ciValue === '1' || String(Cypress.env('CYPRESS_CI') ?? '') === '1'
6+
const apiBase = isCI ? `${appUrl}/api/v1` : 'http://localhost:8080/api/v1'
7+
8+
function getReduxStoreFromWindow (win) {
9+
const rootNode = win.document.getElementById('root')
10+
const reactRootKey = Object.keys(rootNode).find(key => key.startsWith('__reactContainer$') || key.startsWith('__reactFiber$'))
11+
const reactRoot = rootNode[reactRootKey]
12+
const initialFiber = reactRoot.current ? reactRoot.current : (reactRoot.stateNode?.current || reactRoot)
13+
const queue = [initialFiber]
14+
while (queue.length > 0) {
15+
const fiber = queue.shift()
16+
if (fiber?.memoizedProps?.store?.getState) {
17+
return fiber.memoizedProps.store
18+
}
19+
if (fiber?.child) queue.push(fiber.child)
20+
if (fiber?.sibling) queue.push(fiber.sibling)
21+
}
22+
throw new Error('Redux store not found')
23+
}
24+
25+
function getDeviceToken () {
26+
return cy.request('POST', `${apiBase}/device`, {
27+
device_name: 'cypress-local-mcp-snapshot'
28+
}).then((startResp) => {
29+
expect(startResp.status, 'device start status').to.eq(200)
30+
const deviceId = startResp.body.device_id
31+
const authUrl = startResp.body.auth_url
32+
expect(deviceId, 'device_id').to.be.a('string').and.not.eq('')
33+
expect(authUrl, 'auth_url').to.be.a('string').and.include('/device/authorize')
34+
35+
cy.setDevClaimsEmail('test@gmail.com')
36+
cy.visit(authUrl)
37+
cy.contains('button', 'Authorize', { timeout: 20000 }).click()
38+
cy.contains('Device authorized', { timeout: 20000 }).should('be.visible')
39+
40+
return cy.request('POST', `${apiBase}/device/token`, { device_id: deviceId }).then((tokenResp) => {
41+
expect(tokenResp.status, 'device token status').to.eq(200)
42+
expect(tokenResp.body.status, 'device token response status').to.eq('authorized')
43+
expect(tokenResp.body.token, 'device token').to.be.a('string').and.not.eq('')
44+
return tokenResp.body.token
45+
})
46+
})
47+
}
48+
49+
function callMCP (token, name, args = {}) {
50+
return cy.request({
51+
method: 'POST',
52+
url: `${apiBase}/mcp/call`,
53+
headers: {
54+
Authorization: `Bearer ${token}`
55+
},
56+
body: {
57+
name,
58+
arguments: args
59+
},
60+
failOnStatusCode: false
61+
}).then((response) => {
62+
expect(response.status, `${name} http status`).to.eq(200)
63+
expect(response.body).to.have.property('result')
64+
return response.body.result
65+
})
66+
}
67+
68+
function readId (obj, candidates) {
69+
for (const key of candidates) {
70+
const value = obj?.[key]
71+
if (typeof value === 'string' && value.length > 0) return value
72+
}
73+
return ''
74+
}
75+
76+
function buildMapConfig (mapState) {
77+
return JSON.stringify({
78+
version: 'v1',
79+
config: {
80+
visState: {
81+
filters: [],
82+
layers: [],
83+
effects: [],
84+
interactionConfig: {
85+
tooltip: {
86+
fieldsToShow: {},
87+
compareMode: false,
88+
compareType: 'absolute',
89+
enabled: true
90+
},
91+
brush: { size: 0.5, enabled: false },
92+
geocoder: { enabled: false },
93+
coordinate: { enabled: false }
94+
},
95+
layerBlending: 'normal',
96+
overlayBlending: 'normal',
97+
splitMaps: [],
98+
animationConfig: { currentTime: null, speed: 1 },
99+
editor: { features: [], visible: true }
100+
},
101+
mapState: {
102+
bearing: 0,
103+
dragRotate: false,
104+
latitude: mapState.lat,
105+
longitude: mapState.lon,
106+
pitch: 0,
107+
zoom: mapState.zoom,
108+
isSplit: false,
109+
isViewportSynced: true,
110+
isZoomLocked: false,
111+
splitMapViewports: []
112+
},
113+
mapStyle: {
114+
styleType: 'dark',
115+
topLayerGroups: {},
116+
visibleLayerGroups: {
117+
label: true,
118+
road: true,
119+
border: false,
120+
building: true,
121+
water: true,
122+
land: true,
123+
'3d building': false
124+
},
125+
threeDBuildingColor: [9.665468314072013, 17.18305478057247, 31.1442867897876],
126+
backgroundColor: [0, 0, 0],
127+
mapStyles: {}
128+
},
129+
uiState: { mapControls: { mapLegend: { active: false } } }
130+
}
131+
})
132+
}
133+
134+
function expectSnapshotMapState (expected) {
135+
cy.window({ timeout: 60000 }).should((win) => {
136+
const store = getReduxStoreFromWindow(win)
137+
const mapState = store.getState().keplerGl.kepler.mapState
138+
expect(mapState.latitude, 'snapshot latitude').to.be.closeTo(expected.lat, 0.000001)
139+
expect(mapState.longitude, 'snapshot longitude').to.be.closeTo(expected.lon, 0.000001)
140+
expect(mapState.zoom, 'snapshot zoom').to.be.closeTo(expected.zoom, 0.000001)
141+
})
142+
}
143+
144+
describe('local MCP snapshot viewport params', () => {
145+
it('opens snapshot render URL with transient zoom, lat, and lon overrides', () => {
146+
const saved = { lat: 37.7749, lon: -122.4194, zoom: 9 }
147+
const override = { lat: 52.52, lon: 13.405, zoom: 12 }
148+
149+
getDeviceToken().then((token) => {
150+
callMCP(token, 'create_report').then((reportResult) => {
151+
const reportId = readId(reportResult, ['report_id', 'reportId', 'id']) ||
152+
readId(reportResult?.report, ['id'])
153+
expect(reportId, 'report_id').to.be.a('string').and.not.eq('')
154+
155+
return callMCP(token, 'update_report_map_config', {
156+
report_id: reportId,
157+
map_config: buildMapConfig(saved)
158+
}).then(() => reportId)
159+
}).then((reportId) => {
160+
return callMCP(token, 'create_report_snapshot', {
161+
report_id: reportId,
162+
zoom: override.zoom,
163+
lat: override.lat,
164+
lon: override.lon
165+
})
166+
}).then((snapshot) => {
167+
const renderUrl = snapshot.snapshot_render_url || snapshot.snapshotRenderUrl
168+
expect(renderUrl, 'snapshot_render_url').to.be.a('string')
169+
expect(renderUrl).to.include('zoom=12')
170+
expect(renderUrl).to.include('lat=52.52')
171+
expect(renderUrl).to.include('lon=13.405')
172+
173+
cy.visit(renderUrl)
174+
cy.contains('Untitled Report', { timeout: 60000 }).should('not.exist')
175+
expectSnapshotMapState(override)
176+
})
177+
})
178+
})
179+
})

proto/dekart.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ message SaveMapPreviewResponse {
8989

9090
message CreateReportSnapshotRequest {
9191
string report_id = 1; // Existing report id visible to caller.
92+
optional double zoom = 2; // Optional initial snapshot zoom override.
93+
optional double lat = 3; // Optional initial snapshot latitude override.
94+
optional double lon = 4; // Optional initial snapshot longitude override.
9295
}
9396

9497
message CreateReportSnapshotResponse {

proto/dekart_pb.d.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ export class CreateReportSnapshotRequest extends jspb.Message {
4747
getReportId(): string;
4848
setReportId(value: string): void;
4949

50+
hasZoom(): boolean;
51+
clearZoom(): void;
52+
getZoom(): number;
53+
setZoom(value: number): void;
54+
55+
hasLat(): boolean;
56+
clearLat(): void;
57+
getLat(): number;
58+
setLat(value: number): void;
59+
60+
hasLon(): boolean;
61+
clearLon(): void;
62+
getLon(): number;
63+
setLon(value: number): void;
64+
5065
serializeBinary(): Uint8Array;
5166
toObject(includeInstance?: boolean): CreateReportSnapshotRequest.AsObject;
5267
static toObject(includeInstance: boolean, msg: CreateReportSnapshotRequest): CreateReportSnapshotRequest.AsObject;
@@ -60,6 +75,9 @@ export class CreateReportSnapshotRequest extends jspb.Message {
6075
export namespace CreateReportSnapshotRequest {
6176
export type AsObject = {
6277
reportId: string,
78+
zoom: number,
79+
lat: number,
80+
lon: number,
6381
}
6482
}
6583

0 commit comments

Comments
 (0)