-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Embedding SDK - integration commit #40198
Conversation
Codenotify: Notifying subscribers in CODENOTIFY files for diff 4543a7f...21428df.
|
|
7657e1b
to
2bfd09e
Compare
6a3712b
to
ebd4ef2
Compare
children: React.ReactNode; | ||
config: SDKConfigType; | ||
}): JSX.Element => { | ||
const store = getStore(reducers); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this component is wrapped in memo, but isn't it a bit risky to create the store on each "render"?
Docs says "But React may still re-render it: memoization is a performance optimization, not a guarantee."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I moved store initialization out of the component
<EmotionCacheProvider> | ||
<ThemeProvider> | ||
<SdkContentWrapper font={font}> | ||
{!isInitialized ? <div>Initializing...</div> : children} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be wrapped in t
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
return ( | ||
<LoadingAndErrorWrapper | ||
className="flex-full full-width" | ||
loading={!result} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're using loading={!result}
here but isRunning={state.loading}
later on, should we use state.loading
for both?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was copy pasted from the original QueryVisualization component, but it makes sense to refactor this a bit.
Added a fix
@@ -0,0 +1,6 @@ | |||
export type SDKConfigType = { | |||
metabaseInstanceUrl: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: should we just call it "instanceUrl" to help with white-labeling customers?
There's a slack thread about that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the thread I see that we don't really care about source code part.
Here it will also make it harder for developers to use the SDK.
metabaseInstanceUrl
is more readable than instanceUrl
. Developer who will configure this would have to think which value to put here.
const { font, setFont } = useEmbeddingContext(); | ||
|
||
return { | ||
availableFonts: useSelector(state => getSetting(state, "available-fonts")), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the first time I see a hook inside a return 😮 , it makes sense though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't reviewed the webpack config. I think Uladzimir work with that a lot, it could be great to have him review it as well.
frontend/src/metabase/lib/api.js
Outdated
@@ -89,6 +99,15 @@ export class Api extends EventEmitter { | |||
delete headers["Content-Type"]; | |||
} | |||
|
|||
if (this.apiKey) { | |||
headers["x-api-key"] = this.apiKey; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Just so the header name follows the standard naming convention.
headers["x-api-key"] = this.apiKey; | |
headers["X-Api-Key"] = this.apiKey; |
setIsInitialized(true); | ||
setIsLoggedIn(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it be possible that either refreshCurrentUser
or reloadSettings
would fail? i.e. if it returns 401 or something because the token is wrong., should the promise still be resolved? It just feels like we should check the result from calling these endpoints before setting isLoggedIn
to true just to make sure we can call these endpoints.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we have Promise.all
. API calls inside refreshCurrentUser
and reloadSettings
have await
s, so in case any call fails, we would not get to .then
.
We should add some error handling for sure, something like an ErrorBoundary
in QueryVisualizationSdk
component, but I don't think this fits in this PR scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, I found that we reject anything not within 2xx status, so this is good. I was worried errored requests are still going through .then
metabase/frontend/src/metabase/lib/api.js
Lines 202 to 212 in dc279fd
if (status >= 200 && status <= 299) { | |
if (options.transformResponse) { | |
body = options.transformResponse(body, { data }); | |
} | |
resolve(body); | |
} else { | |
reject({ | |
status: status, | |
data: body, | |
isCancelled: isCancelled, | |
}); |
|
||
useEffect(() => { | ||
if (font) { | ||
dispatch(setOptions({ font })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we setting any option other than font
in SDK right now? I'm asking because setOptions
overrides all the embed config. If we set it to something else else where and change font, other settings will be reverted to default values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now we don't set any options other than font
. But that's a valid concern for sure! I will add a comment to address in future.
isLoggedIn, | ||
}} | ||
> | ||
<SdkContentWrapper data-elementid="sdk-content-wrapper" font={font}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- What is
data-elementid
for? I couldn't find its reference in the app. - I kind of get we pass
font
here because it's used inSdkContentWrapper
, but why do we need to set it insetOptions
too? Isn't setting font inSdkContentWrapper
enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! We might don't need to have it in our state as we don't use GlobalStyles
component from the main app, which is connected to redux state.
In our case it is passed to us from the outside, so it's not logical to have it exposed from useAvailableFonts
hook - users already know which value they use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And what is data-elementid
for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just a way for us to detect this Global styles component on a page.
We can remove it for now.
); | ||
}; | ||
|
||
export const MetabaseProvider = memo(MetabaseProviderInternal); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be nice, if you include an example how this provider is supposed to be used in enterprise/frontend/src/embedding-sdk/README.md
. Now that readme file is pretty barebone.
It's hard to just imagine how this component is supposed to be used without context.
Otherwise, we could have a long comment above this file documenting how it's supposed to be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example on how to use SDK usage would be self-described in the host app example - https://github.com/metabase/embedding-sdk-customer-zero/blob/pre-alpha/src/App.js#L20 .
- we will have types for public components added in the next task.
But I will add an action item to add documentation for the SDK at some point.
frontend/src/metabase/lib/api.js
Outdated
|
||
GET; | ||
POST; | ||
PUT; | ||
DELETE; | ||
|
||
constructor() { | ||
constructor(basename) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
constructor(basename) { | |
constructor() { |
You could remove line 50-51 as well.
I don't think you pass anything to the constructor right now.
metabase/frontend/src/metabase/lib/api.js
Line 311 in 504840b
const instance = new Api(); |
This is being set to the exported object directly here.
api.apiKey = config.apiKey; |
@@ -20,7 +20,7 @@ export const refreshSiteSettings = createAsyncThunk( | |||
); | |||
|
|||
export const settings = createReducer( | |||
{ values: window.MetabaseBootstrap, loading: false }, | |||
{ values: window.MetabaseBootstrap || {}, loading: false }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed? Have some types changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In our SDK initialization logic, we don't have window.MetabaseBootstrap available.
In the regular app, BE injects this data into generated html using this script in this html template. There were some errors because we have undefined
instead of an object. So, we can either add a default value to the global window object, or add it here. In my opinion the latter is a bit cleaner
@@ -153,7 +153,7 @@ export default class ChoroplethMap extends Component { | |||
this.setState({ | |||
geoJson: geoJson, | |||
geoJsonPath: geoJsonPath, | |||
minimalBounds: computeMinimalBounds(geoJson.features), | |||
minimalBounds: computeMinimalBounds(geoJson?.features ?? []), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed? Have some types changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We had an issue that this line was empty in our case. We might be missing some predefined state or initialization steps, but with this change it seems to be working as it should be
"exclude": [ | ||
"node_modules", | ||
// The following files will load `frontend/src/metabase/app` which will conflict with | ||
// `frontend/src/metabase/App.tsx`, since now we're loading both JS and TS files. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably should do something about this (not in this PR though). VS Code confuses frontend/src/metabase/App.tsx
with frontend/src/metabase/app.js
and that's frustrated when working with these files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe renaming app.js
to init.js
could help, as it seems it's just code to init the app
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, that would do. We don't have init.*
in frontend/src/metabase/
currently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been copied from the regular https://github.com/metabase/metabase/blob/master/tsconfig.json#L23 used in the main app.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I know that because I wrote that comment 😅 . It's just that this is something we should be aware of at least because I'm not particularly satisfied with the solution I went with either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already a lot better, but I'd like more people to have a look at the webpack config.
cardError: null, | ||
resultError: null, | ||
}); | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I like the else
part more compared to the previous solution. I was thinking something felt off, but I couldn't think of anything else at that time.
setIsInitialized(true); | ||
setIsLoggedIn(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, I found that we reject anything not within 2xx status, so this is good. I was worried errored requests are still going through .then
metabase/frontend/src/metabase/lib/api.js
Lines 202 to 212 in dc279fd
if (status >= 200 && status <= 299) { | |
if (options.transformResponse) { | |
body = options.transformResponse(body, { data }); | |
} | |
resolve(body); | |
} else { | |
reject({ | |
status: status, | |
data: body, | |
isCancelled: isCancelled, | |
}); |
isLoggedIn, | ||
}} | ||
> | ||
<SdkContentWrapper data-elementid="sdk-content-wrapper" font={font}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And what is data-elementid
for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some minor comments re webpack config. It's very similar to our main webpack config.
Overall comments:
- webpack config can be reused, we really override just a few lines, so no need to copy everything, especially it will become a nightmare in support
- nit. It looks like we can keep
package.json.template
with empty dependencies if the idea is to use it only for publishing and remove.template
in the pre-publish step. It can potentially save us from unnecessary node_modules installation.
webpack.embedding-sdk.config.js
Outdated
{ | ||
loader: "style-loader", | ||
}, | ||
// { loader: MiniCssExtractPlugin.loader }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I tried it in a main webpack config and it broke half of the styles
webpack.embedding-sdk.config.js
Outdated
path: BUILD_PATH + "/dist", | ||
publicPath: "", | ||
filename: "[name].bundle.js", | ||
libraryTarget: "commonjs2", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://webpack.js.org/configuration/output/#type-commonjs2
output: {
library: {
// note there's no `name` here
type: 'commonjs2',
},
},
new webpack.ProvidePlugin({ | ||
process: "process/browser.js", | ||
}), | ||
new ForkTsCheckerWebpackPlugin({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a reason to use it instead of normal tsc at CI, but if you have arguments 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will use it to generate .d.ts
files for SDK bundle later on
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't tsc enough for it?
@deniskaber Did you forget to add a milestone to the issue for this PR? When and where should I add a milestone? |
Relates to #40241
Description
Embedding SDK code.
How to verify
Demo
Checklist