Skip to content
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

Merged
merged 14 commits into from
Mar 26, 2024
Merged

Embedding SDK - integration commit #40198

merged 14 commits into from
Mar 26, 2024

Conversation

deniskaber
Copy link
Contributor

@deniskaber deniskaber commented Mar 15, 2024

Relates to #40241

Description

Embedding SDK code.

How to verify

Demo

Checklist

  • Tests have been added/updated to cover changes in this PR

@deniskaber deniskaber self-assigned this Mar 15, 2024
@deniskaber deniskaber added no-backport Do not backport this PR to any branch .Team/Embedding labels Mar 15, 2024
@deniskaber deniskaber changed the title Embedding SDK - initial commit Embedding SDK - integration commit Mar 15, 2024
@metabase-bot metabase-bot bot added the visual Run Percy visual testing label Mar 15, 2024
Copy link

github-actions bot commented Mar 15, 2024

Codenotify: Notifying subscribers in CODENOTIFY files for diff 4543a7f...21428df.

Notify File(s)
@alxnddr frontend/src/metabase/visualizations/components/ChoroplethMap.jsx

Copy link

replay-io bot commented Mar 15, 2024

Status Complete ↗︎
Commit 21428df
Results
⚠️ 6 Flaky
2378 Passed

@deniskaber deniskaber requested a review from a team March 21, 2024 19:07
children: React.ReactNode;
config: SDKConfigType;
}): JSX.Element => {
const store = getStore(reducers);
Copy link
Member

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."

Copy link
Contributor Author

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

@npretto npretto requested a review from a team March 22, 2024 12:52
<EmotionCacheProvider>
<ThemeProvider>
<SdkContentWrapper font={font}>
{!isInitialized ? <div>Initializing...</div> : children}
Copy link
Member

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?

Copy link
Contributor Author

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}
Copy link
Member

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?

Copy link
Contributor Author

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;
Copy link
Member

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

Copy link
Contributor Author

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")),
Copy link
Member

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deniskaber deniskaber requested review from npretto and a team March 22, 2024 16:44
Copy link
Member

@WiNloSt WiNloSt left a 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.

@@ -89,6 +99,15 @@ export class Api extends EventEmitter {
delete headers["Content-Type"];
}

if (this.apiKey) {
headers["x-api-key"] = this.apiKey;
Copy link
Member

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.

Suggested change
headers["x-api-key"] = this.apiKey;
headers["X-Api-Key"] = this.apiKey;

Comment on lines +46 to +47
setIsInitialized(true);
setIsLoggedIn(true);
Copy link
Member

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?

Copy link
Contributor Author

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 awaits, 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.

Copy link
Member

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

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 }));
Copy link
Member

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.

Copy link
Contributor Author

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}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. What is data-elementid for? I couldn't find its reference in the app.
  2. I kind of get we pass font here because it's used in SdkContentWrapper, but why do we need to set it in setOptions too? Isn't setting font in SdkContentWrapper enough?

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

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);
Copy link
Member

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.

Copy link
Contributor Author

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.


GET;
POST;
PUT;
DELETE;

constructor() {
constructor(basename) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
constructor(basename) {
constructor() {

You could remove line 50-51 as well.

I don't think you pass anything to the constructor right now.

const instance = new Api();

This is being set to the exported object directly here.

frontend/src/metabase/lib/api.js Outdated Show resolved Hide resolved
@@ -20,7 +20,7 @@ export const refreshSiteSettings = createAsyncThunk(
);

export const settings = createReducer(
{ values: window.MetabaseBootstrap, loading: false },
{ values: window.MetabaseBootstrap || {}, loading: false },
Copy link
Member

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?

Copy link
Contributor Author

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 ?? []),
Copy link
Member

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?

Copy link
Contributor Author

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.
Copy link
Member

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.

Copy link
Member

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

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

@deniskaber deniskaber requested a review from WiNloSt March 25, 2024 18:11
@deniskaber deniskaber requested a review from a team March 25, 2024 18:11
Copy link
Member

@WiNloSt WiNloSt left a 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 {
Copy link
Member

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.

Comment on lines +46 to +47
setIsInitialized(true);
setIsLoggedIn(true);
Copy link
Member

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

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}>
Copy link
Member

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?

Copy link
Contributor

@uladzimirdev uladzimirdev left a 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:

  1. 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
  2. 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.

{
loader: "style-loader",
},
// { loader: MiniCssExtractPlugin.loader },
Copy link
Contributor

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

path: BUILD_PATH + "/dist",
publicPath: "",
filename: "[name].bundle.js",
libraryTarget: "commonjs2",
Copy link
Contributor

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({
Copy link
Contributor

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 👍

Copy link
Contributor Author

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

Copy link
Contributor

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 deniskaber enabled auto-merge (squash) March 26, 2024 20:39
@deniskaber deniskaber merged commit 98f6cd8 into master Mar 26, 2024
109 checks passed
@deniskaber deniskaber deleted the embedding-sdk-initial branch March 26, 2024 23:10
Copy link

@deniskaber Did you forget to add a milestone to the issue for this PR? When and where should I add a milestone?

@deniskaber deniskaber added this to the 0.50 milestone Mar 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no-backport Do not backport this PR to any branch .Team/Embedding visual Run Percy visual testing
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants