-
Notifications
You must be signed in to change notification settings - Fork 272
[SIP-4] add lerna monorepo and@superset-ui/core
package with SupersetClient
#1
Conversation
.gitignore
Outdated
@@ -0,0 +1,30 @@ | |||
# Logs |
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.
Personally I would just enumurate these in a single alphabetical list as it’s easier to process and maintain.
PULL_REQUEST_TEMPLATE.md
Outdated
@@ -0,0 +1,9 @@ | |||
💔 Breaking Changes |
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.
Can this be added to the .github folder?
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! I'll give it a shot
timeout: this.timeout, | ||
url: this.getUrl({ endpoint: 'superset/csrf_token/', host: this.host }), | ||
}) | ||
.then(response => { |
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.
Perhaps use .then(onFulfilled, onRejected)
instead of .then(onFulfilled).catch(onRejected)
.catch(onRejected)
is equivalent to.then(undefined, onRejected)
The current catch
clause will handle Promise.reject
thrown from the then
clause as well, which has the same result in this case, but is redundant work.
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.
sure, I think the .catch
is a little more readable but don't have a super strong opinion.
} | ||
|
||
getCSRFToken() { | ||
this.requestingCsrf = 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.
Instead of having boolean flags this.requestingCsrf
, we can store the promise returned from callApi()
in this.requestingCsrf
. The initial value of this.requestingCsrf
can be Promise.reject('not initialized')
.
This may remove the need for this.waitForCSRF
and polling/setTimeout mechanism as this.ensureAuth
can return this.requestingCsrf;
directly.
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.
that's a great idea, I'll try to move toward that 👍
return Promise.race([ | ||
new Promise((resolve, reject) => { | ||
if (typeof timeout === 'number') { | ||
timeoutId = setTimeout( |
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.
Perhaps create another function called createTimer
.
function createTimer(timeout) {
return new Promise((resolve, reject) => {
setTimeout(() => reject(...), timeout);
});
}
To avoid worrying about clearing timeout or passing around timeoutId
, can break the callApi
function into two parts
function callApiWithoutParsing() {
// the first half of original callApi()
// handle params and return fetch();
}
function parseResponse(apiPromise) {
return apiPromise.then( .... ).then( .... ); // do the parsing
}
function callApi() {
return parseResponse(callApiWithoutParsing());
}
If timeout
is not a number, there is no need to race.
function callApiWithTimeout() {
const apiPromise = callApiWithoutParsing(...);
// This will race until results come back from server
// excluding parsing time
const racedPromise = (typeof timeout === 'number')
? Promise.race([createTimer(timeout), apiPromise])
: apiPromise;
return parseResponse(racedPromise);
}
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.
done, my only concern here is what happens to the timeouts without clearing them (when say a dashboard makes 15 requests for chart data). I couldn't find much info online about this, assume they're garbage collected.
}); | ||
} | ||
|
||
return Promise.resolve(typeof text === 'undefined' ? { json, response } : { response, text }); |
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 resolve
is extraneous, will remove. the rejection
above is useful to forward the error.
@@ -0,0 +1,497 @@ | |||
/* eslint promise/no-callback-in-promise: 'off' */ | |||
import sinon from 'sinon'; |
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.
just learned more about jest spys, should be able to move this
packages/superset-ui-core/README.md
Outdated
SupersetClient.get({}).then(...).catch(...); | ||
|
||
if (IWantToCancelForSomeReason) { | ||
signal.abort(); // Promise is rejected, request `catch` is invoked |
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.
Does it cancel every outgoing request? Is there a way to cancel just one?
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.
oops I forgot an important part of this usage (I'll update), you pass the signal to the request, so it's per-request aborting.
see here for an example in superset.
const apiPromise = callApi(rest); | ||
|
||
const racedPromise = | ||
typeof timeout === 'number' |
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.
&& timeout > 0
return typeof text === 'undefined' ? { json, response } : { response, text }; | ||
}), | ||
// forward the error | ||
error => Promise.reject(error), |
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 line is probably not necessary as error propagate through already
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.
linting has complained is some places that we need a catch
for every then
, but it doesn't here 🤷♀️
|
||
const mockGetPayload = { get: 'payload' }; | ||
const mockPostPayload = { post: 'payload' }; | ||
const mockErrorPayload = { status: 500, statusText: 'Internal errorz!' }; |
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.
errorz
. typo?
@@ -0,0 +1,156 @@ | |||
/* eslint promise/no-callback-in-promise: 'off' */ |
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.
Awesome for having tests for all of these.
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.
learned a lot about jest!
} | ||
|
||
get({ host, url, endpoint, mode, credentials, headers, body, timeout, signal }) { | ||
return this.ensureAuth().then(() => |
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 do you think about attaching an abort
function to the returned promise from get
and post
?
const promise = this.ensureAuth().then(...);
promise.abort = signal && () => signal.abort();
return promise;
So can use like this.
const myPromise = SupersetClient.get(...);
if(wannaCancel && myPromise.abort) {
myPromise.abort();
}
The concern is the function abort()
will only be available to the original Promise returned from get
/post
and not on the chained then()
, which may be not obvious to developer.
Another thing is instead of creating a signal
from outside and passing it in as an argument, can we construct the signal
in get
/post
or callApi
so all calls get abort functionality by default?
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.
hmm, that's an interesting thought, the abort controller api is a little verbose. I do think it would be non-ideal to have the abort
just attached to the initial promise.
maybe we can see how it goes with signal
and we could develop a nicer / more complex api around aborting in the future? there were only a couple of places in Superset that use this 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.
in practice you either have to pass the abort controller around, or the promise, and so I'm not sure there's much advantage of one over the other except that the former is full-fledged API spec not just our library API.
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.
sgtm. We can try that later. I think this first version looks good to me.
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.
Overall lgtm. Left one question for the getCSRFToken()
but not blocking.
} | ||
|
||
get({ host, url, endpoint, mode, credentials, headers, body, timeout, signal }) { | ||
return this.ensureAuth().then(() => |
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.
sgtm. We can try that later. I think this first version looks good to me.
import rejectAfterTimeout from '../../src/callApi/rejectAfterTimeout'; | ||
import throwIfCalled from '../utils/throwIfCalled'; | ||
|
||
describe('rejectAfterTimeout', () => { |
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
Add ()
in describe('rejectAfterTimeout()'
) so we know it is a function when reading test result
import { LOGIN_GLOB } from '../fixtures/constants'; | ||
import throwIfCalled from '../utils/throwIfCalled'; | ||
|
||
describe('parseResponse', () => { |
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
Add ()
in describe('parseResponse()'
) so we know it is a function when reading test result
import { LOGIN_GLOB } from '../fixtures/constants'; | ||
import throwIfCalled from '../utils/throwIfCalled'; | ||
|
||
describe('callApiAndParseWithTimeout', () => { |
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
Add ()
in describe('callApiAndParseWithTimeout()'
) so we know it is a function when reading test result
import { LOGIN_GLOB } from '../fixtures/constants'; | ||
import throwIfCalled from '../utils/throwIfCalled'; | ||
|
||
describe('callApi', () => { |
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
Add ()
in describe('callApi()'
) so we know it is a function when reading test result
return Promise.reject({ error: 'Failed to fetch CSRF token' }); | ||
} | ||
|
||
return response; |
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 return response
or this.csrfToken
?
Nobody is really using its result at this point, but seems like this.csrfToken
will be more expected?
If somebody called this multiple times, it will send new request every time. Is that expected behavior? or if a promise already exists, then return this.csrfPromise
so it will send request only once.
…#1) fix: tooltip disappearance and stickiness
This PR
superset-ui
monorepo (i.e., a repository of multiple packages) withlerna
and@superset-core
, which implementsSupersetClient
as described in SIP-4.SupersetClient
is pulled from step 2 this PR, which was abandoned for smaller steps, but supports all the client-side async functionality encompassed by the current Superset app.Below are some more details about the monorepo +
@superset-ui/core
package, see the README (link soon) for API/usage details for theSupersetClient
.Monorepo overview
lerna
is used to manage versions and dependencies betweenpackages in this monorepo.
To develop:
After this you can run scripts across the entire monorep (e.g.,
yarn run test
runs tests across all repos) or within a given package. The latter means that each package defines its own build config, linting, and testing. I initially set it up to have shared things likejest
,prettier
, andlinting
but I think this will be more painful down the line. I think we can achieve consistency with yeoman package generators which @kristw and I will do in a future PR.@superset-ui/core
After this PR is merged, we can publish the first version of this package.
@data-ui/build-config
is used to manage allbabel
,jest
testing,eslint
,prettier
. This type of "shareable" build config will save a lot of copy/paste as the # of packages grows.Testing
We should have good coverage for client-side code from the start 🔥 In a future PR we'll set up code coverage but here are the stats for what I wrote 🚀
@kristw @conglei @mistercrunch @graceguo-supercat @john-bodley @hughhhh @betodealmeida @michellethomas