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

[chore] Refactored cloud provider flow for performance and multi provider support #2436

Merged
merged 4 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 10 additions & 10 deletions examples/demo-app/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ const GlobalStyle = styled.div`
}
`;

const CONTAINER_STYLE = {
transition: 'margin 1s, height 1s',
position: 'absolute',
width: '100%',
height: '100%',
left: 0,
top: 0
};

class App extends Component {
state = {
showBanner: false,
Expand Down Expand Up @@ -416,16 +425,7 @@ class App extends Component {
>
<Announcement onDisable={this._disableBanner} />
</Banner>
<div
style={{
transition: 'margin 1s, height 1s',
position: 'absolute',
width: '100%',
height: '100%',
left: 0,
top: 0
}}
>
<div style={CONTAINER_STYLE}>
<AutoSizer>
{({height, width}) => (
<KeplerGl
Expand Down
232 changes: 118 additions & 114 deletions examples/demo-app/src/cloud-providers/dropbox/dropbox-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default class DropboxProvider extends Provider {
this.appName = appName;

this._folderLink = `${KEPLER_DROPBOX_FOLDER_LINK}/${appName}`;
this._path = `/Apps/${window.decodeURIComponent(this.appName)}`;
this._path = '';

// Initialize Dropbox API
this._initializeDropbox();
Expand All @@ -73,71 +73,62 @@ export default class DropboxProvider extends Provider {
* - Receive the token when ready
* - Close the opened tab
*/
async login(onCloudLoginSuccess) {
const link = this._authLink();

const authWindow = window.open(link, '_blank', 'width=1024,height=716');
async login() {
return new Promise((resolve, reject) => {
const link = this._authLink();

const handleToken = async e => {
// TODO: add security step to validate which domain the message is coming from
if (authWindow) {
authWindow.close();
}
const authWindow = window.open(link, '_blank', 'width=1024,height=716');

window.removeEventListener('message', handleToken);
const handleToken = async event => {
// if user has dev tools this will skip all the react-devtools events
if (!event.data.token) {
return;
}

if (!e.data.token) {
Console.warn('Failed to login to Dropbox');
return;
}
if (authWindow) {
authWindow.close();
window.removeEventListener('message', handleToken);
}

this._dropbox.setAccessToken(e.data.token);
// save user name
const user = await this._getUser();

if (window.localStorage) {
window.localStorage.setItem(
'dropbox',
JSON.stringify({
// dropbox token doesn't expire unless revoked by the user
token: e.data.token,
user,
timestamp: new Date()
})
);
}
const {token} = event.data;

if (typeof onCloudLoginSuccess === 'function') {
onCloudLoginSuccess();
}
};

window.addEventListener('message', handleToken);
}
if (!token) {
reject('Failed to login to Dropbox');
return;
}

async downloadMap(loadParams) {
const token = this.getAccessToken();
if (!token) {
this.login(() => this.downloadMap(loadParams));
}
const result = await this._dropbox.filesDownload(loadParams);
const json = await this._readFile(result.fileBlob);
this._dropbox.setAccessToken(token);
// save user name
const user = await this.getUser();

if (window.localStorage) {
window.localStorage.setItem(
'dropbox',
JSON.stringify({
// dropbox token doesn't expire unless revoked by the user
token: token,
user,
timestamp: new Date()
})
);
}

const response = {
map: json,
format: 'keplergl'
};
resolve(user);
};

this._loadParam = loadParams;
return response;
window.addEventListener('message', handleToken);
});
}

/**
* returns a list of maps
*/
async listMaps() {
// list files
try {
// https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesListFolder__anchor
const response = await this._dropbox.filesListFolder({
path: this._path
path: `${this._path}`
});
const {pngs, visualizations} = this._parseEntries(response);
// https://dropbox.github.io/dropbox-sdk-js/Dropbox.html#filesGetThumbnailBatch__anchor
Expand All @@ -148,15 +139,14 @@ export default class DropboxProvider extends Provider {
);

// append to visualizations
thumbnails &&
thumbnails.forEach(thb => {
if (thb['.tag'] === 'success' && thb.thumbnail) {
const matchViz = visualizations[pngs[thb.metadata.id] && pngs[thb.metadata.id].name];
if (matchViz) {
matchViz.thumbnail = `${IMAGE_URL_PREFIX}${thb.thumbnail}`;
}
(thumbnails || []).forEach(thb => {
if (thb['.tag'] === 'success' && thb.thumbnail) {
const matchViz = visualizations[pngs[thb.metadata.id] && pngs[thb.metadata.id].name];
if (matchViz) {
matchViz.thumbnail = `${IMAGE_URL_PREFIX}${thb.thumbnail}`;
}
});
}
});

// dropbox returns
return Object.values(visualizations).reverse();
Expand All @@ -166,49 +156,10 @@ export default class DropboxProvider extends Provider {
}
}

getUserName() {
// load user from
if (window.localStorage) {
const jsonString = window.localStorage.getItem('dropbox');
const user = jsonString && JSON.parse(jsonString).user;
return user;
}
return null;
}

async logout(onCloudLogoutSuccess) {
const token = this._dropbox.getAccessToken();

if (token) {
await this._dropbox.authTokenRevoke();
if (window.localStorage) {
window.localStorage.removeItem('dropbox');
}
// re instantiate dropbox
this._initializeDropbox();
onCloudLogoutSuccess();
}
}

isEnabled() {
return this.clientId !== null;
}

hasPrivateStorage() {
return PRIVATE_STORAGE_ENABLED;
}

hasSharingUrl() {
return SHARING_ENABLED;
}

/**
*
* @param mapData map data and config in one json object {map: {datasets: Array<Object>, config: Object, info: Object}
* @param blob json file blob to upload
* @param fileName if blob doesn't contain a file name, this field is used
* @param isPublic define whether the file will be available publicly once uploaded
* @returns {Promise<DropboxTypes.files.FileMetadata>}
*/
async uploadMap({mapData, options = {}}) {
const {isPublic} = options;
Expand All @@ -221,10 +172,11 @@ export default class DropboxProvider extends Provider {
// FileWriteMode: Selects what to do if the file already exists.
// Always overwrite if sharing
const mode = options.overwrite || isPublic ? 'overwrite' : 'add';
const path = `${this._path}/${fileName}`;
let metadata;
try {
metadata = await this._dropbox.filesUpload({
path: `${this._path}/${fileName}`,
path,
contents: JSON.stringify(fileContent),
mode
});
Expand All @@ -233,13 +185,15 @@ export default class DropboxProvider extends Provider {
throw this.getFileConflictError();
}
}

// save a thumbnail image
thumbnail &&
(await this._dropbox.filesUpload({
path: `${this._path}/${fileName}`.replace(/\.json$/, '.png'),
if (thumbnail) {
await this._dropbox.filesUpload({
path: path.replace(/\.json$/, '.png'),
contents: thumbnail,
mode
}));
});
}

// keep on create shareUrl
if (isPublic) {
Expand All @@ -252,6 +206,58 @@ export default class DropboxProvider extends Provider {
return this._loadParam;
}

/**
* download the map content
* @param loadParams
*/
async downloadMap(loadParams) {
const token = this.getAccessToken();
if (!token) {
this.login(() => this.downloadMap(loadParams));
}
const result = await this._dropbox.filesDownload(loadParams);
const json = await this._readFile(result.fileBlob);

const response = {
map: json,
format: 'keplergl'
};

this._loadParam = loadParams;
return response;
}

getUserName() {
// load user from
if (window.localStorage) {
const jsonString = window.localStorage.getItem('dropbox');
const user = jsonString && JSON.parse(jsonString).user;
return user;
}
return null;
}

async logout() {
await this._dropbox.authTokenRevoke();
if (window.localStorage) {
window.localStorage.removeItem('dropbox');
}
// re instantiate dropbox
this._initializeDropbox();
}

isEnabled() {
return this.clientId !== null;
}

hasPrivateStorage() {
return PRIVATE_STORAGE_ENABLED;
}

hasSharingUrl() {
return SHARING_ENABLED;
}

/**
* Get the share url of current map, this url can be accessed by anyone
* @param {boolean} fullUrl
Expand Down Expand Up @@ -314,22 +320,15 @@ export default class DropboxProvider extends Provider {
this._dropbox.setClientId(this.clientId);
}

async _getUser() {
let response;
try {
response = await this._dropbox.usersGetCurrentAccount();
} catch (error) {
Console.warn(error);
return null;
}

async getUser() {
const response = await this._dropbox.usersGetCurrentAccount();
return this._getUserFromAccount(response);
}

_handleDropboxError(error) {
// dropbox list_folder error
if (error && error.error && error.error.error_summary) {
return `Dropbox Error: ${error.error.error_summary}`;
return new Error(`Dropbox Error: ${error.error.error_summary}`);
}

return error;
Expand Down Expand Up @@ -422,7 +421,12 @@ export default class DropboxProvider extends Provider {
}

_getUserFromAccount(response) {
return response ? (response.name && response.name.abbreviated_name) || response.email : null;
const {name} = response;
return {
name: name.display_name,
email: response.email,
abbreviated: name.abbreviated_name
};
}

_getThumbnailRequests(pngs) {
Expand Down
2 changes: 1 addition & 1 deletion examples/demo-app/src/cloud-providers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import DropboxProvider from './dropbox/dropbox-provider';
import CartoProvider from './carto/carto-provider';

const {DROPBOX_CLIENT_ID, CARTO_CLIENT_ID} = AUTH_TOKENS;
const DROPBOX_CLIENT_NAME = 'Kepler.gl%20(managed%20by%20Uber%20Technologies%2C%20Inc.)';
const DROPBOX_CLIENT_NAME = 'Kepler.gl Demo App';

export const DEFAULT_CLOUD_PROVIDER = 'dropbox';

Expand Down
2 changes: 1 addition & 1 deletion examples/demo-app/src/factories/map-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import React from 'react';
import styled from 'styled-components';
import {
withState,
withState,
MapControlFactory,
EffectControlFactory,
EffectManagerFactory
Expand Down
6 changes: 5 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ const config = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
verbose: true,
testMatch: ['<rootDir>/src/**/*.spec.js', '<rootDir>/test/**/*.spec.js'],
testPathIgnorePatterns: [
// ignore all dist computed directories
"<rootDir>/.*(/|\\\\)dist(/|\\\\).*"
],
testMatch: ['<rootDir>/src/**/*.spec.(ts|tsx)', '<rootDir>/src/**/*.spec.js', '<rootDir>/test/**/*.spec.js'],
// Per https://jestjs.io/docs/configuration#transformignorepatterns-arraystring, transformIgnorePatterns ignores
// node_modules and pnp folders by default so that they are not transpiled
// Some libraries (even if transitive) are transitioning to ESM and need additional transpilation. Relevant issues:
Expand Down