@hopara/iframe 2.4.57 → 2.4.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ import { EventType, HOPARA_EVENT_TYPE, LoadDataRequestData } from './Events'
2
+ import { DataLoader, FilterSet, LoaderResponse } from '@hopara/dataset'
3
+ import { Row } from '@hopara/dataset'
4
+
5
+ export class EventEmitter {
6
+ private static sendMessage(data: any, targetWindow?: Window) {
7
+ const target = targetWindow || window.parent || window.top
8
+ if (!window) return
9
+ // eslint-disable-next-line @microsoft/sdl/no-postmessage-star-origin
10
+ target.postMessage(data, '*')
11
+ }
12
+
13
+ static ready(): void {
14
+ this.sendMessage({[HOPARA_EVENT_TYPE]: EventType.READY})
15
+ }
16
+
17
+ static init(config: any, targetWindow: Window): void {
18
+ this.sendMessage(Object.assign(config, {[HOPARA_EVENT_TYPE]: EventType.INIT}), targetWindow)
19
+ }
20
+
21
+ static loadDataRequest(dataLoader: Partial<DataLoader>, filterSet: FilterSet): void {
22
+ this.sendMessage({
23
+ [HOPARA_EVENT_TYPE]: EventType.LOAD_DATA,
24
+ data: {query: dataLoader.query, name: dataLoader.query, source: dataLoader.source},
25
+ filterSet,
26
+ } as LoadDataRequestData)
27
+ }
28
+
29
+ static getRows(response: any, error?: Error) {
30
+ if (error || !response) {
31
+ return []
32
+ }
33
+
34
+ return Array.isArray(response) ? response : response.rows
35
+ }
36
+
37
+ static getColumns(response:any, error?: Error) {
38
+ if (error) {
39
+ return []
40
+ }
41
+
42
+ return Array.isArray(response) ? [] : response.columns
43
+ }
44
+
45
+ static getError(response:any, error?: Error) {
46
+ if (error) {
47
+ return error
48
+ }
49
+
50
+ if (!response) {
51
+ return new Error('Load data with undefined response')
52
+ }
53
+
54
+ return undefined
55
+ }
56
+
57
+
58
+ static loadDataResponse(config: any, dataLoader: Partial<DataLoader>, response: any[] | LoaderResponse, error: Error | undefined, targetWindow: Window): void {
59
+ const rows = this.getRows(response, error)
60
+ const columns = this.getColumns(response, error)
61
+ this.sendMessage(Object.assign(
62
+ config,
63
+ {[HOPARA_EVENT_TYPE]: EventType.LOAD_DATA_RESPONSE},
64
+ {data: {query: dataLoader.query ?? (dataLoader as any).name, source: dataLoader.source},
65
+ rows, columns, error: this.getError(response, error)},
66
+ ), targetWindow)
67
+ }
68
+
69
+ static refresh(config: any, targetWindow: Window): void {
70
+ this.sendMessage({
71
+ ...config,
72
+ [HOPARA_EVENT_TYPE]: EventType.REFRESH,
73
+ }, targetWindow)
74
+ }
75
+
76
+ static update(config:any, targetWindow: Window): void {
77
+ this.sendMessage({
78
+ ...config,
79
+ [HOPARA_EVENT_TYPE]: EventType.UPDATE,
80
+ }, targetWindow)
81
+ }
82
+
83
+ static callback(name: string, row: Row): void {
84
+ this.sendMessage({
85
+ [HOPARA_EVENT_TYPE]: EventType.FUNCTION_CALLBACK,
86
+ name,
87
+ row,
88
+ })
89
+ }
90
+ }
@@ -0,0 +1,24 @@
1
+ import { DataLoader, LoaderResponse } from '@hopara/dataset'
2
+ import { EventType, HOPARA_EVENT_TYPE, LoadDataResponseData, PostMessageEvent } from './Events'
3
+
4
+ export class EventReceiver {
5
+ static isHoparaMessage(event: any): boolean {
6
+ return event.data[HOPARA_EVENT_TYPE] !== undefined
7
+ }
8
+
9
+ static loadDataResponse(dataLoader: Partial<DataLoader>): Promise<LoaderResponse> {
10
+ return new Promise((resolve) => {
11
+ const callback = (event: PostMessageEvent<LoadDataResponseData>) => {
12
+ if (
13
+ event.data[HOPARA_EVENT_TYPE] === EventType.LOAD_DATA_RESPONSE &&
14
+ (event.data.data.query === dataLoader.query || (event.data.data as any).name === dataLoader.query) &&
15
+ event.data.data.source === dataLoader.source) {
16
+ window.removeEventListener('message', callback, false)
17
+ resolve({rows: event.data.rows ?? [], columns: event.data.columns ?? [], error: event.data.error})
18
+ }
19
+ }
20
+
21
+ window.addEventListener('message', callback, false)
22
+ })
23
+ }
24
+ }
@@ -0,0 +1,86 @@
1
+ import { ConfigEnvironment } from '@hopara/config'
2
+ import { FilterSet } from '@hopara/dataset'
3
+ import { Position } from '@hopara/components/src/view-state/ViewState'
4
+ import { SelectedFilter } from '@hopara/components/src/filter/domain/SelectedFilter'
5
+ import { InitialRow } from '@hopara/components/src/initial-row/InitialRow'
6
+ import { PlainError } from '@hopara/dataset/src/repository/LoaderDatasetRepository'
7
+ import { PlainDataLoader } from '@hopara/dataset/src/loader/DataLoader'
8
+ import { HoparaTab } from '../view/Provider'
9
+ import { Language } from '@hopara/browser'
10
+
11
+ export enum EventType {
12
+ INIT = 'init',
13
+ READY = 'ready',
14
+ UPDATE = 'update',
15
+ LOAD_DATA = 'loadData',
16
+ LOAD_DATA_RESPONSE = 'loadDataResponse',
17
+ UPDATE_DATA = 'updateData',
18
+ UPDATE_DATA_RESPONSE = 'updateDataResponse',
19
+ REFRESH = 'refresh',
20
+ FUNCTION_CALLBACK = 'functionCallback',
21
+ }
22
+
23
+ export const HOPARA_EVENT_TYPE = '__hopara__eventType__'
24
+
25
+ export interface EventData {
26
+ [HOPARA_EVENT_TYPE]?: EventType
27
+ }
28
+
29
+ export interface InitData extends EventData {
30
+ accessToken?: string
31
+ refreshToken?: string
32
+ visualizationId?: string
33
+ fallbackVisualizationId?: string
34
+ visualizationScope?: string
35
+ tenant?: string
36
+ env?: ConfigEnvironment
37
+ initialPosition?: Position
38
+ initialRow?: InitialRow
39
+ dataLoaders?: PlainDataLoader[]
40
+ darkMode?: boolean
41
+ callbackNames?: string[]
42
+ filters?: Partial<SelectedFilter>[]
43
+ language?: Language | `${Language}`
44
+ initialTab?: HoparaTab | `${HoparaTab}`
45
+ navigationControls?: boolean
46
+ }
47
+
48
+ export interface LoadDataRequestData extends EventData {
49
+ data: {
50
+ query: string
51
+ source: string
52
+ }
53
+ filterSet: FilterSet
54
+ }
55
+
56
+ export interface LoadDataResponseData extends EventData {
57
+ data: {
58
+ query: string
59
+ source: string
60
+ }
61
+ rows: any[]
62
+ columns: any[]
63
+ error?: PlainError
64
+ }
65
+
66
+ export interface CallbackFunctionData extends EventData {
67
+ name: string
68
+ row: any
69
+ }
70
+
71
+ export interface PostMessageEvent<D> extends MessageEvent {
72
+ origin: string
73
+ data: D
74
+ }
75
+
76
+ export const isReadyEvent = (event: PostMessageEvent<EventData>): event is PostMessageEvent<EventData> => {
77
+ return event.data[HOPARA_EVENT_TYPE] === EventType.READY
78
+ }
79
+
80
+ export const isLoadDataEvent = (event: PostMessageEvent<EventData>): event is PostMessageEvent<LoadDataRequestData> => {
81
+ return event.data[HOPARA_EVENT_TYPE] === EventType.LOAD_DATA
82
+ }
83
+
84
+ export const isCallbackFunctionEvent = (event: PostMessageEvent<EventData>): event is PostMessageEvent<CallbackFunctionData> => {
85
+ return event.data[HOPARA_EVENT_TYPE] === EventType.FUNCTION_CALLBACK
86
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ // react create app needs this file to work properly
2
+ export const anyFn = () => ({})
@@ -0,0 +1,9 @@
1
+ // / <reference types="react-scripts" />
2
+
3
+ declare module 'math.gl';
4
+ declare module 'class-transformer';
5
+ declare module '*.css';
6
+ declare module '*.svg';
7
+ declare module '*.png';
8
+ declare module '*.gif';
9
+ declare module '*.glb';
@@ -0,0 +1 @@
1
+ import '@hopara/service-worker'
@@ -0,0 +1,258 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import {connect} from 'react-redux'
3
+ import jwtDecode from 'jwt-decode'
4
+ import { Logger } from '@hopara/internals'
5
+ import actions, { HocLoadedPayload } from '@hopara/components/src/hoc/HocActions'
6
+ import { InitialRow } from '@hopara/components/src/initial-row/InitialRow'
7
+ import { HoparaController } from '@hopara/components/src/hoc/HoparaController'
8
+ import { Config, ConfigEnvironment } from '@hopara/config'
9
+ import { CallbackFunction } from '@hopara/components/src/action/ActionReducer'
10
+ import { SelectedFilter } from '@hopara/components/src/filter/domain/SelectedFilter'
11
+ import { SelectedFilters } from '@hopara/components/src/filter/domain/SelectedFilters'
12
+ import { Authorization } from '@hopara/authorization'
13
+ import { Store } from '@hopara/components/src/state/Store'
14
+ import { getPageUrl } from '@hopara/page/src/PageNavigation'
15
+ import { PageType } from '@hopara/page/src/Pages'
16
+ import { useNavigate } from 'react-router-dom'
17
+ import { FilterStore } from '@hopara/components/src/filter/state/FilterStore'
18
+ import QueryStore from '@hopara/components/src/query/QueryStore'
19
+ import { isEqual } from 'lodash/fp'
20
+ import { PlainDataLoader } from '@hopara/dataset/src/loader/DataLoader'
21
+ import { Language } from '@hopara/browser'
22
+
23
+ export enum HoparaTab {
24
+ OBJECTS='objects',
25
+ LAYERS='layers',
26
+ SETTINGS='settings',
27
+ VISUALIZATION='visualization'
28
+ }
29
+
30
+ export type EmbeddedProps = {
31
+ env?: ConfigEnvironment
32
+ visualizationId: string
33
+ fallbackVisualizationId?: string
34
+ visualizationScope?: string
35
+ accessToken: string
36
+ refreshToken?: string
37
+ tenant?: string
38
+ dataLoaders?: PlainDataLoader[]
39
+ initialRow?: InitialRow
40
+ controller?: HoparaController
41
+ darkMode?: boolean
42
+ callbacks?: CallbackFunction[]
43
+ attribution?: boolean
44
+ filters?: Partial<SelectedFilter>[]
45
+ language?: Language
46
+ initialTab?: HoparaTab
47
+ navigationControls?: boolean
48
+ }
49
+
50
+ const logInit = (message:string, props: EmbeddedProps) => {
51
+ Logger.debug(message, {
52
+ visualizationId: props.visualizationId,
53
+ accessToken: props.accessToken ? `${props.accessToken?.substring(0, 50)}...` : undefined,
54
+ })
55
+ }
56
+
57
+ const getDecodedToken = (accessToken: string) => {
58
+ try {
59
+ return jwtDecode<any>(accessToken)
60
+ } catch {
61
+ return {}
62
+ }
63
+ }
64
+
65
+ interface ProviderStateProps extends EmbeddedProps {
66
+ stateVisualizationId: string
67
+ stateFallbackVisualizationId: string
68
+ stateTenant: string
69
+ stateAuthorization?: Authorization
70
+ filterStore: FilterStore
71
+ queryStore: QueryStore
72
+ }
73
+
74
+ interface ProviderStateActions {
75
+ onLoad: (navigateToTabUrl) => void
76
+ onUpdate: () => void
77
+ onVisualizationChanged: (navigateFn: any, tenant: string) => void
78
+ forceRefresh: () => void
79
+ onTabChange: (navigateFn: any, tab: HoparaTab) => void
80
+ }
81
+
82
+ const Provider = (props: ProviderStateProps & ProviderStateActions) => {
83
+ const navigate = useNavigate()
84
+ const [isFirstLoad, setIsFirstLoad] = useState(true)
85
+ const [oldVisualization, setOldVisualizationId] = useState({id: props.visualizationId, fallback: props.fallbackVisualizationId})
86
+ const [oldToken, setOldToken] = useState({token: props.accessToken, refreshToken: props.refreshToken, tenant: props.tenant})
87
+ const [oldFilters, setOldFilters] = useState(props.filters)
88
+ const [oldDataLoaders, setOldDataLoaders] = useState(props.dataLoaders)
89
+ const [oldInitialRow, setOldInitialRow] = useState(props.initialRow)
90
+ const [oldInitialTab, setOldInitialTab] = useState(props.initialTab)
91
+
92
+ const isRenderable = !!props.accessToken && !!props.visualizationId
93
+ const isTokenRefreshable = !!props.accessToken && !!props.refreshToken
94
+
95
+ const hasVisualizationChanged = !isEqual(oldVisualization.id, props.visualizationId) || !isEqual(oldVisualization.fallback, props.fallbackVisualizationId)
96
+ const hasTokenChanged = !isTokenRefreshable && (!isEqual(oldToken.token, props.accessToken) || !isEqual(oldToken.refreshToken, props.refreshToken))
97
+ const hasFiltersChanged = !isEqual(oldFilters, props.filters)
98
+ const hasDataLoadersChanged = !isEqual(oldDataLoaders, props.dataLoaders)
99
+ const hasInitialRowChanged = !isEqual(oldInitialRow, props.initialRow)
100
+ const hasInitialTabChanged = !isEqual(oldInitialTab, props.initialTab)
101
+
102
+ const shouldUpdate = hasVisualizationChanged || hasTokenChanged || hasFiltersChanged || hasDataLoadersChanged || hasInitialRowChanged || hasInitialTabChanged
103
+
104
+ useEffect(() => {
105
+ if (!shouldUpdate || !isRenderable) return
106
+ if (props.controller) props.controller.setRefreshSignal(props.forceRefresh)
107
+ if (props.env && props.env !== Config.environment) Config.setEnv(props.env)
108
+
109
+ setOldToken({token: props.accessToken, refreshToken: props.refreshToken, tenant: props.tenant})
110
+ setOldVisualizationId({id: props.visualizationId, fallback: props.fallbackVisualizationId})
111
+ setOldFilters(props.filters)
112
+ setOldDataLoaders(props.dataLoaders)
113
+ setOldInitialRow(props.initialRow)
114
+ setOldInitialTab(props.initialTab)
115
+
116
+ if (isFirstLoad) {
117
+ setIsFirstLoad(false)
118
+ return props.onLoad((navigateToTabUrl) => navigate(navigateToTabUrl, {replace: false, state: {jumpBack: false}}))
119
+ } else if (hasVisualizationChanged) {
120
+ props.onVisualizationChanged((url) => navigate(url, {replace: false, state: {jumpBack: false}}), props.tenant ?? props.stateTenant)
121
+ } else if (hasInitialTabChanged && props.initialTab) {
122
+ props.onTabChange((url) => navigate(url, {replace: false, state: {jumpBack: false}}), props.initialTab)
123
+ } else {
124
+ props.onUpdate()
125
+ }
126
+ }, [props.visualizationId, props.accessToken, props.refreshToken, props.tenant, props.filters, props.dataLoaders, props.initialTab, shouldUpdate])
127
+
128
+ return <></>
129
+ }
130
+
131
+ const tabToPageType = (tab: HoparaTab): PageType => {
132
+ switch (tab) {
133
+ case HoparaTab.LAYERS:
134
+ return PageType.VisualizationLayerEditor
135
+ case HoparaTab.OBJECTS:
136
+ return PageType.VisualizationObjectEditor
137
+ case HoparaTab.SETTINGS:
138
+ return PageType.VisualizationSettings
139
+ default:
140
+ return PageType.VisualizationDetail
141
+ }
142
+ }
143
+
144
+ const mapState = (state: Store, props: EmbeddedProps): ProviderStateProps => {
145
+ return {
146
+ ...props,
147
+ tenant: props.tenant,
148
+ stateTenant: state.auth.authorization.tenant,
149
+ visualizationId: props.visualizationId ?? '',
150
+ stateVisualizationId: state.visualizationStore.visualization?.id ?? '',
151
+ stateFallbackVisualizationId: state.visualizationStore.fallbackVisualizationId ?? '',
152
+ stateAuthorization: state.auth.authorization,
153
+ filterStore: state.filterStore,
154
+ queryStore: state.queryStore,
155
+ }
156
+ }
157
+
158
+ const mapActions = (dispatch: any, props: EmbeddedProps): ProviderStateActions => {
159
+ const getActionPayload = (): HocLoadedPayload => {
160
+ const {tenants = [], scope = '', exp = 0, username} = getDecodedToken(props.accessToken)
161
+ const authorization = new Authorization({
162
+ accessToken: props.accessToken,
163
+ refreshToken: props.refreshToken,
164
+ expiration: exp,
165
+ tenant: props.tenant ?? tenants[0],
166
+ tenants,
167
+ permissions: scope?.split(' '),
168
+ clientId: username,
169
+ })
170
+
171
+ const filters = props.filters?.length ?
172
+ new SelectedFilters(...props.filters.map((filter) => new SelectedFilter(filter))) :
173
+ undefined
174
+
175
+ return {
176
+ visualizationId: props.visualizationId,
177
+ fallbackVisualizationId: props.fallbackVisualizationId,
178
+ visualizationScope: props.visualizationScope,
179
+ dataLoaders: props.dataLoaders,
180
+ initialRow: props.initialRow,
181
+ darkMode: !!props.darkMode,
182
+ callbacks: props.callbacks,
183
+ filters,
184
+ authorization,
185
+ language: props.language,
186
+ navigationControls: props.navigationControls,
187
+ }
188
+ }
189
+
190
+ return {
191
+ onLoad: (navigate) => {
192
+ logInit('initializing module', props)
193
+
194
+ const actionPayload = getActionPayload()
195
+ dispatch(actions.configLoaded(actionPayload))
196
+ if (!props.initialTab) return
197
+
198
+ const initalTabURL = getPageUrl(
199
+ tabToPageType(props.initialTab),
200
+ actionPayload.authorization.tenant,
201
+ {
202
+ visualizationId: props.visualizationId,
203
+ fallbackVisualizationId: props.fallbackVisualizationId,
204
+ visualizationScope: props.visualizationScope,
205
+ initialLayerId: props.initialRow?.layerId,
206
+ initialRowId: props.initialRow?.rowId,
207
+ filters: props.filters,
208
+ },
209
+ )
210
+
211
+ return navigate(initalTabURL)
212
+ },
213
+ onTabChange: (navigate: any, tab: HoparaTab) => {
214
+ const actionPayload = getActionPayload()
215
+ const initalTabURL = getPageUrl(
216
+ tabToPageType(tab),
217
+ actionPayload.authorization.tenant,
218
+ {
219
+ visualizationId: props.visualizationId,
220
+ fallbackVisualizationId: props.fallbackVisualizationId,
221
+ visualizationScope: props.visualizationScope,
222
+ initialLayerId: props.initialRow?.layerId,
223
+ initialRowId: props.initialRow?.rowId,
224
+ filters: props.filters,
225
+ },
226
+ )
227
+
228
+ return navigate(initalTabURL)
229
+ },
230
+ onUpdate: () => {
231
+ logInit('updating module', props)
232
+ dispatch(actions.configUpdated(getActionPayload()))
233
+ },
234
+ onVisualizationChanged: (navigate, tenant) => {
235
+ if (!tenant) {
236
+ tenant = getActionPayload().authorization.tenant
237
+ }
238
+
239
+ const url = getPageUrl(
240
+ PageType.VisualizationDetail,
241
+ tenant,
242
+ {
243
+ visualizationId: props.visualizationId,
244
+ fallbackVisualizationId: props.fallbackVisualizationId,
245
+ visualizationScope: props.visualizationScope,
246
+ initialLayerId: props.initialRow?.layerId,
247
+ initialRowId: props.initialRow?.rowId,
248
+ filters: props.filters,
249
+ },
250
+ )
251
+
252
+ return navigate(url)
253
+ },
254
+ forceRefresh: () => dispatch(actions.forceRefresh()),
255
+ }
256
+ }
257
+
258
+ export const EmbeddedProvider = connect(mapState, mapActions)(Provider)
@@ -0,0 +1,35 @@
1
+ import React, {lazy, Suspense} from 'react'
2
+ import {Route, Routes} from 'react-router-dom'
3
+ import {Pages, PageType} from '@hopara/page/src/Pages'
4
+ import {LoadingSpinner} from '@hopara/design-system/src/loading-spinner/Spinner'
5
+ import {ConnectedAuthProvider} from '@hopara/components/src/hoc/AuthProvider'
6
+ import {RumRouteMonitoring} from '@hopara/rum/src/RumMonitoring'
7
+
8
+ const VisualizationOutlet = lazy(() => import(/* webpackPreload: true */ '@hopara/components/src/visualization/pages/VisualizationOutlet'))
9
+ const VisualizationPageTemplate = lazy(() => import(/* webpackPreload: true */ '@hopara/components/src/visualization/pages/VisualizationPageTemplate'))
10
+ const ObjectEditorOutlet = lazy(() => import(/* webpackPreload: true */ '@hopara/components/src/object/editor/ObjectEditorOutletContainer'))
11
+ const SettingsOutlet = lazy(() => import(/* webpackPreload: true */ '@hopara/components/src/settings/SettingsOutletContainer'))
12
+ const LayerEditorOutlet = lazy(() => import(/* webpackPreload: true */ '@hopara/components/src/layer/editor/LayerEditorOutletContainer'))
13
+ const ForbiddenPage = lazy(() => import(/* webpackPreload: true */ '@hopara/design-system/src/static-pages/ForbiddenPage'))
14
+ const NotFoundPage = lazy(() => import(/* webpackPreload: true */ '@hopara/design-system/src/static-pages/NotFoundPage'))
15
+
16
+ export const Router = () => {
17
+ return (
18
+ <ConnectedAuthProvider>
19
+ <Suspense fallback={<LoadingSpinner fullscreen={true}/>}>
20
+ <RumRouteMonitoring />
21
+ <Routes>
22
+ <Route path={Pages.getPath(PageType.Forbidden)} element={<ForbiddenPage/>}/>
23
+ <Route path={Pages.getPath(PageType.NotFound)} element={<NotFoundPage/>}/>
24
+ <Route element={<VisualizationPageTemplate/>}>
25
+ <Route path={Pages.getPath(PageType.VisualizationDetail)} element={<VisualizationOutlet/>}/>
26
+ <Route path={Pages.getPath(PageType.VisualizationObjectEditor)} element={<ObjectEditorOutlet/>}/>
27
+ <Route path={Pages.getPath(PageType.VisualizationSettings)} element={<SettingsOutlet/>}/>
28
+ <Route path={Pages.getPath(PageType.VisualizationLayerEditor)} element={<LayerEditorOutlet/>}/>
29
+ </Route>
30
+ <Route path="*" element={<LoadingSpinner fullscreen={true}/>}/>
31
+ </Routes>
32
+ </Suspense>
33
+ </ConnectedAuthProvider>
34
+ )
35
+ }
@@ -0,0 +1,159 @@
1
+ import React from 'react'
2
+ import {MemoryRouter} from 'react-router-dom'
3
+ import {EmbeddedProvider, EmbeddedProps} from './Provider'
4
+ import { HoparaController } from '@hopara/components/src/hoc/HoparaController'
5
+ import { EventType, HOPARA_EVENT_TYPE, InitData, PostMessageEvent } from '../events/Events'
6
+ import { EventReceiver } from '../events/EventReceiver'
7
+ import { EventEmitter } from '../events/EventEmitter'
8
+ import {Router} from './Router'
9
+ import { PureComponent } from '@hopara/design-system'
10
+ import { Box } from '@mui/material'
11
+ import HoparaProvider from '@hopara/components/src/hoc/Provider'
12
+ import { DataLoader } from '@hopara/dataset'
13
+ import { PlainDataLoader } from '@hopara/dataset/src/loader/DataLoader'
14
+
15
+ type Props = {
16
+ children?: React.ReactElement
17
+ }
18
+
19
+ export class View extends PureComponent<Props, EmbeddedProps> {
20
+ constructor(props) {
21
+ super(props)
22
+ this.state = {
23
+ accessToken: '',
24
+ visualizationId: '',
25
+ fallbackVisualizationId: undefined,
26
+ visualizationScope: undefined,
27
+ env: undefined,
28
+ refreshToken: undefined,
29
+ tenant: undefined,
30
+ dataLoaders: undefined,
31
+ initialRow: undefined,
32
+ controller: undefined,
33
+ darkMode: undefined,
34
+ callbacks: undefined,
35
+ attribution: undefined,
36
+ filters: undefined,
37
+ language: undefined,
38
+ initialTab: undefined,
39
+ navigationControls: undefined,
40
+ }
41
+ }
42
+
43
+ private dataLoaderAsPostMessage(dataLoader: PlainDataLoader): DataLoader {
44
+ const migratedDataLoader = {
45
+ query: dataLoader.query ?? (dataLoader as any).name,
46
+ source: dataLoader.source,
47
+ cache: dataLoader.cache,
48
+ }
49
+
50
+ return new DataLoader({
51
+ ...migratedDataLoader,
52
+ loader: async (filterSet) => {
53
+ EventEmitter.loadDataRequest(migratedDataLoader, filterSet)
54
+ return EventReceiver.loadDataResponse(migratedDataLoader)
55
+ },
56
+ })
57
+ }
58
+
59
+ private callbacksAsPostMessage(name: string): any {
60
+ return {
61
+ name,
62
+ callback: (data) => EventEmitter.callback(name, data),
63
+ }
64
+ }
65
+ private getVisualizationId(event:PostMessageEvent<InitData>): string {
66
+ return event.data.visualizationId ?? event.data.fallbackVisualizationId!
67
+ }
68
+
69
+ handleInitEvent(event:PostMessageEvent<InitData>, callback?: () => void) {
70
+ const newState = Object.keys(this.state).reduce((state, key) => {
71
+ // we only update the state if the key is present in the new event data
72
+ return { ...state, [key]: key in event.data ? event.data[key] : state[key] }
73
+ }, this.state)
74
+
75
+ this.setState({
76
+ ...newState,
77
+ accessToken: event.data.accessToken!,
78
+ visualizationId: this.getVisualizationId(event),
79
+ callbacks: event.data.callbackNames?.map(this.callbacksAsPostMessage.bind(this)),
80
+ dataLoaders: event.data.dataLoaders?.map(this.dataLoaderAsPostMessage.bind(this)),
81
+ controller: new HoparaController(),
82
+ }, callback)
83
+ }
84
+
85
+ private shouldSkipEvent(event:PostMessageEvent<any>): boolean {
86
+ const isValidEvent = event.data.__hopara__eventType__ === EventType.INIT ||
87
+ event.data.__hopara__eventType__ === EventType.UPDATE ||
88
+ event.data.__hopara__eventType__ == EventType.REFRESH
89
+ const hasVisualizationId = !!event.data.visualizationId ||
90
+ !!event.data.visualization ||
91
+ !!event.data.app ||
92
+ !!event.data.fallbackVisualizationId
93
+
94
+ return !isValidEvent || !event.data.accessToken || !hasVisualizationId
95
+ }
96
+
97
+ initMessageListener(event:PostMessageEvent<InitData>) {
98
+ if (this.shouldSkipEvent(event)) return
99
+
100
+ switch (event.data[HOPARA_EVENT_TYPE]) {
101
+ case (EventType.INIT):
102
+ case (EventType.UPDATE):
103
+ return this.handleInitEvent(event)
104
+ case (EventType.REFRESH):
105
+ return this.state.controller?.refresh()
106
+ }
107
+ }
108
+
109
+ cachedInitMessageListener: EventListenerOrEventListenerObject
110
+
111
+ registerToReceiveMessage() {
112
+ if (!window) return
113
+
114
+ this.cachedInitMessageListener = this.initMessageListener.bind(this) as any
115
+ window.addEventListener('message', this.cachedInitMessageListener, false)
116
+
117
+ EventEmitter.ready()
118
+ }
119
+
120
+ cachedRegisterToReceiveMessage: any
121
+
122
+ componentDidMount(): void {
123
+ this.cachedRegisterToReceiveMessage = this.registerToReceiveMessage.bind(this)
124
+ window.addEventListener('load', this.cachedRegisterToReceiveMessage, false)
125
+ }
126
+
127
+ unregisterToReceiveMessage() {
128
+ if (!window) return
129
+
130
+ if (this.cachedRegisterToReceiveMessage) {
131
+ window.removeEventListener('load', this.cachedRegisterToReceiveMessage, false)
132
+ }
133
+
134
+ if (this.cachedInitMessageListener) {
135
+ window.removeEventListener('message', this.cachedInitMessageListener, false)
136
+ }
137
+ }
138
+
139
+ componentWillUnmount(): void {
140
+ this.unregisterToReceiveMessage()
141
+ }
142
+
143
+ render() {
144
+ return (
145
+ <Box sx={{
146
+ 'width': '100vw',
147
+ 'height': '100vh',
148
+ '@supports (height: 100dvh)': { height: '100dvh' },
149
+ }}>
150
+ <HoparaProvider>
151
+ <MemoryRouter>
152
+ <EmbeddedProvider {...this.state as any} />
153
+ <Router />
154
+ </MemoryRouter>
155
+ </HoparaProvider>
156
+ </Box>
157
+ )
158
+ }
159
+ }