Skip to content

Commit

Permalink
[feat] Introduce Foursquare cloud provider (#2437)
Browse files Browse the repository at this point in the history
Signed-off-by: Giuseppe Macri <gmacri@foursquare.com>
  • Loading branch information
macrigiuseppe committed Nov 18, 2023
1 parent 82d616e commit d60ef31
Show file tree
Hide file tree
Showing 27 changed files with 431 additions and 139 deletions.
21 changes: 11 additions & 10 deletions docs/api-reference/cloud-providers/README.md
Expand Up @@ -43,20 +43,21 @@ An instance of the provider is added to array of cloud providers in the file `sr
import {Provider} from 'kepler.gl/cloud-providers';

class MyProvider extends Provider {
constructor() {
this.name = 'foo';
this.displayName = 'My Provider';
}
// ... other required methods below
constructor() {
this.name = 'foo';
this.displayName = 'My Provider';
}

// ... other required methods below
}

const myProvider = new MyProvider();
const App = () =>
<KeplerGl
mapboxApiAccessToken={AUTH_TOKENS.MAPBOX_TOKEN}
id="map"
cloudProviders={[myProvider]}
/>
<KeplerGl
mapboxApiAccessToken={CLOUD_PROVIDERS_CONFIGURATION.MAPBOX_TOKEN}
id="map"
cloudProviders={[myProvider]}
/>
```


Expand Down
22 changes: 11 additions & 11 deletions docs/api-reference/cloud-providers/cloud-provider.md
Expand Up @@ -23,9 +23,9 @@ The default provider class

**Parameters**

- `props` **[object][27]**
- `props.name` **[string][28]**
- `props.displayName` **[string][28]**
- `props` **[object][27]**
- `props.name` **[string][28]**
- `props.displayName` **[string][28]**
- `props.icon` **ReactElement** React element
- `props.thumbnail` **[object][27]** thumbnail size object
- `props.thumbnail.width` **[number][29]** thumbnail width in pixels
Expand Down Expand Up @@ -111,13 +111,13 @@ Returns **[string][28]** true if a user already logged in

Whether this provider support upload map to a private storage. If truthy, user will be displayed with the storage save icon on the top right of the side bar.

Returns **[boolean][31]**
Returns **[boolean][31]**

### hasSharingUrl

Whether this provider support share map via a public url, if truthy, user will be displayed with a share map via url under the export map option on the top right of the side bar

Returns **[boolean][31]**
Returns **[boolean][31]**

### listMaps

Expand All @@ -133,7 +133,7 @@ async listMaps() {
title: 'My map',
description: 'My first kepler map',
imageUrl: 'http://',
lastModification: 1582677787000,
udpatedAt: 1582677787000,
privateMap: false,
loadParams: {}
}
Expand Down Expand Up @@ -168,7 +168,7 @@ With the option to overwrite already saved map, and upload as private or public

**Parameters**

- `param` **[Object][27]**
- `param` **[Object][27]**
- `param.mapData` **[Object][27]** the map object
- `param.mapData.map` **[Object][27]** {datasets. config, info: {title, description}}
- `param.mapData.thumbnail` **[Blob][35]** A thumbnail of current map. thumbnail size can be defined by provider by this.thumbnail
Expand All @@ -186,10 +186,10 @@ Type: [Object][27]

### Properties

- `map` **[Object][27]**
- `map.datasets` **[Array][32]&lt;[Object][27]>**
- `map.config` **[Object][27]**
- `map.info` **[Object][27]**
- `map` **[Object][27]**
- `map.datasets` **[Array][32]&lt;[Object][27]>**
- `map.config` **[Object][27]**
- `map.info` **[Object][27]**
- `format` **[string][28]** one of 'csv': csv file string, 'geojson': geojson object, 'row': row object, 'keplergl': datasets array saved using KeplerGlSchema.save

## Viz
Expand Down
1 change: 1 addition & 0 deletions examples/demo-app/package.json
Expand Up @@ -10,6 +10,7 @@
"start-local-https": "webpack-dev-server --mode development --https --env.es6 --progress --hot --open"
},
"dependencies": {
"@auth0/auth0-spa-js": "^2.1.2",
"@carto/toolkit": "0.0.1-rc.18",
"@kepler.gl/actions": "^3.0.0-alpha.1",
"@kepler.gl/components": "^3.0.0-alpha.1",
Expand Down
4 changes: 2 additions & 2 deletions examples/demo-app/src/app.js
Expand Up @@ -30,7 +30,7 @@ import Announcement, {FormLink} from './components/announcement';
import {replaceLoadDataModal} from './factories/load-data-modal';
import {replaceMapControl} from './factories/map-control';
import {replacePanelHeader} from './factories/panel-header';
import {AUTH_TOKENS, DEFAULT_FEATURE_FLAGS} from './constants/default-settings';
import {CLOUD_PROVIDERS_CONFIGURATION, DEFAULT_FEATURE_FLAGS} from './constants/default-settings';
import {messages} from './constants/localization';

import {
Expand Down Expand Up @@ -429,7 +429,7 @@ class App extends Component {
<AutoSizer>
{({height, width}) => (
<KeplerGl
mapboxApiAccessToken={AUTH_TOKENS.MAPBOX_TOKEN}
mapboxApiAccessToken={CLOUD_PROVIDERS_CONFIGURATION.MAPBOX_TOKEN}
id="map"
/*
* Specify path to keplerGl state, because it is not mount at the root
Expand Down
Expand Up @@ -21,10 +21,9 @@
// DROPBOX
import {Dropbox} from 'dropbox';
import window from 'global/window';
import Console from 'global/console';
import DropboxIcon from './dropbox-icon';
import {MAP_URI} from '../../constants/default-settings';
import {Provider} from '@kepler.gl/cloud-providers';
import {KEPLER_FORMAT, Provider} from '@kepler.gl/cloud-providers';

const NAME = 'dropbox';
const DISPLAY_NAME = 'Dropbox';
Expand Down Expand Up @@ -220,11 +219,11 @@ export default class DropboxProvider extends Provider {

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

this._loadParam = loadParams;
return response;
return Promise.resolve(response);
}

getUserName() {
Expand Down Expand Up @@ -474,7 +473,7 @@ export default class DropboxProvider extends Provider {
name,
title,
id,
lastModification: new Date(client_modified).getTime(),
updatedAt: new Date(client_modified).getTime(),
loadParams: {
path: path_lower
}
Expand Down
@@ -0,0 +1,41 @@
import React, {Component} from 'react';
import {Icons} from '@kepler.gl/components';
import PropTypes from 'prop-types';

const style = {
background: 'black'
};
export default class FoursquareIcon extends Component {
static propTypes = {
/** Set the height of the icon, ex. '16px' */
height: PropTypes.string,
colors: PropTypes.arrayOf(PropTypes.string)
};

static defaultProps = {
width: '64px',
fill: 'black',
predefinedClassName: 'cloud-provider-studio-icon',
totalColor: 1,
style
};

render() {
return (
<Icons.IconWrapper {...this.props} viewBox="0 -10 84 120">
<path
d="M2.30368 0H33.1681V5.341H8.33118V18.5748H30.1695V23.7674H8.33118V43.5885H2.30368V0Z"
fill="white"
/>
<path
d="M5.66576 84.6965C5.80711 87.1692 6.40279 89.177 7.4528 90.72C9.45187 93.6081 12.9755 95.0521 18.0236 95.0521C20.2852 95.0521 22.3449 94.7356 24.2026 94.1026C27.7969 92.8762 29.594 90.6804 29.594 87.5154C29.594 85.1416 28.8368 83.4503 27.3223 82.4414C25.7877 81.4524 23.3848 80.5919 20.1136 79.86L14.0861 78.5247C10.1485 77.6543 7.36194 76.6949 5.72633 75.6465C2.89937 73.8266 1.48588 71.1067 1.48588 67.4867C1.48588 63.5699 2.86908 60.3554 5.63547 57.8432C8.40186 55.3309 12.3192 54.0748 17.3876 54.0748C22.0521 54.0748 26.0098 55.1826 29.2608 57.3981C32.532 59.5939 34.1676 63.115 34.1676 67.9614H28.5036C28.2007 65.6272 27.5546 63.837 26.5651 62.5907C24.7276 60.3159 21.6078 59.1784 17.2058 59.1784C13.6519 59.1784 11.0976 59.9104 9.54274 61.3742C7.98791 62.838 7.21049 64.5392 7.21049 66.4778C7.21049 68.6142 8.11916 70.1769 9.9365 71.166C11.1279 71.799 13.8236 72.5903 18.0236 73.5398L24.2632 74.9344C27.2719 75.607 29.594 76.5268 31.2296 77.6939C34.0566 79.7314 35.4701 82.6887 35.4701 86.5659C35.4701 91.3926 33.6729 94.8444 30.0786 96.9215C26.5045 98.9985 22.3449 100.037 17.5996 100.037C12.0668 100.037 7.7355 98.6524 4.60564 95.883C1.47579 93.1333 -0.0588526 89.4045 0.00172532 84.6965H5.66576Z"
fill="white"
/>
<path
d="M83.8112 98.6425L80.7823 102.233L73.9067 97.0995C72.2509 97.9897 70.4538 98.7018 68.5153 99.2359C66.597 99.77 64.497 100.037 62.2152 100.037C55.3093 100.037 49.8977 97.8215 45.9803 93.3905C42.5274 89.0781 40.8009 83.6778 40.8009 77.1895C40.8009 71.2946 42.2952 66.2503 45.2837 62.0566C49.1203 56.6761 54.7944 53.9858 62.3061 53.9858C70.161 53.9858 75.9765 56.4585 79.7525 61.4039C82.7006 65.2612 84.1747 70.1967 84.1747 76.2103C84.1747 79.0193 83.8213 81.7194 83.1146 84.3108C82.0444 88.2671 80.2371 91.4915 77.6929 93.9839L83.8112 98.6425ZM62.9421 94.7554C64.1941 94.7554 65.3653 94.6763 66.4557 94.518C67.5461 94.34 68.4951 94.0037 69.3028 93.5092L64.4263 89.7705L67.4552 86.1208L73.2707 90.542C75.1082 88.4847 76.35 86.1801 76.9962 83.6283C77.6626 81.0765 77.9957 78.6335 77.9957 76.2993C77.9957 71.1759 76.6226 67.0515 73.8764 63.926C71.1504 60.8005 67.4148 59.2378 62.6695 59.2378C57.8637 59.2378 54.0574 60.7412 51.2506 63.748C48.4438 66.735 47.0404 71.344 47.0404 77.5752C47.0404 82.8173 48.3833 86.9912 51.0689 90.0969C53.7747 93.2026 57.7324 94.7554 62.9421 94.7554Z"
fill="white"
/>
</Icons.IconWrapper>
);
}
}
@@ -0,0 +1,172 @@
import FSQIcon from './foursquare-icon';
import {Provider, KEPLER_FORMAT} from 'kepler.gl/cloud-providers';
import {Auth0Client} from '@auth0/auth0-spa-js';

const NAME = 'Foursquare';
const DISPLAY_NAME = 'Foursquare';
const APP_NAME = 'Kepler.gl';

const FOURSQUARE_PRIVATE_STORAGE_ENABLED = true;
const FOURSQUARE_AUTH_AUDIENCE = 'https://foursquare.com/api/';
const FOURSQUARE_AUTH_SCOPE = 'openid profile email';

// Foursquare stores kepler maps using kepler.gl-raw as ImportSource
const FOURSQUARE_KEPLER_GL_IMPORT_SOURCE = 'kepler.gl-raw';

/**
* Converts a FSQ map model to cloud provider map item
* @param model Foursquare Map
* @return {MapItem} Map
*/
function convertFSQModelToMapItem(model) {
return {
id: model.id,
title: model.name,
thumbnail: model.previewReadPath,
updatedAt: model.updatedAt,
description: model.description,
loadParams: {
mapId: model.id
}
};
}

function extractMapFromFSQResponse(response) {
const {
latestState: {data}
} = response;
return data;
}

export default class FoursquareProvider extends Provider {
constructor({clientId, authDomain, apiURL, userMapsURL}) {
super({name: NAME, displayName: DISPLAY_NAME, icon: FSQIcon});
this.icon = FSQIcon;
this.appName = APP_NAME;
this.apiURL = apiURL;

const redirect_uri = window.location.origin + window.location.pathname;

this._auth0 = new Auth0Client({
domain: authDomain,
clientId: clientId,
scope: FOURSQUARE_AUTH_SCOPE,
authorizationParams: {
redirect_uri,
audience: FOURSQUARE_AUTH_AUDIENCE
},
cacheLocation: 'localstorage'
});

// the domain needs to be passed as input param
this._folderLink = userMapsURL;
this.isNew = true;
}

hasPrivateStorage() {
return FOURSQUARE_PRIVATE_STORAGE_ENABLED;
}

async getUser() {
return this._auth0.getUser();
}

async login() {
return this._auth0.loginWithPopup();
}

async logout() {
return this._auth0.logout({
// this make sure after logging out the sdk will not redirect the user
openUrl: false
});
}

async uploadMap({mapData, options = {}}) {
// TODO: handle replace
const mode = options.overwrite ? 'overwrite' : 'add';
const method = options.overwrite ? 'PUT' : 'POST'
const {map, thumbnail} = mapData;

const {title = '', description = '', id} = map.info;
const headers = await this.getHeaders();
const payload = {
name: title,
description,
importSource: FOURSQUARE_KEPLER_GL_IMPORT_SOURCE,
latestState: {
data: map
}
};

const mapResponse = await fetch(`${this.apiURL}/v1/maps${mode === 'overwrite' ? `/${id}` : ''}`, {
method,
headers,
body: JSON.stringify(payload)
});

const createMap = await mapResponse.json();

await fetch(`${this.apiURL}/v1/maps/${createMap.id}/thumbnail`, {
method: 'PUT',
headers: {
...headers,
'Content-Type': 'image/png'
},
body: thumbnail
});

return map;
}

async listMaps() {
const headers = await this.getHeaders();
const response = await fetch(
`${this.apiURL}/v1/maps?importSource=${FOURSQUARE_KEPLER_GL_IMPORT_SOURCE}`,
{
method: 'GET',
mode: 'cors',
headers
}
);
const data = await response.json();
return data.items.map(convertFSQModelToMapItem);
}

async downloadMap(loadParams) {
const {mapId} = loadParams;
if (!mapId) {
return Promise.reject('No Map is was provider as part of loadParams');
}
const headers = await this.getHeaders();

const response = await fetch(`${this.apiURL}/v1/maps/${mapId}`, {
method: 'GET',
headers
});

const map = await response.json();

return Promise.resolve({
map: extractMapFromFSQResponse(map),
format: KEPLER_FORMAT
});
}

getManagementUrl() {
return this._folderLink;
}

async getAccessToken() {
return this._auth0.getTokenSilently();
}

async getHeaders() {
const accessToken = await this.getAccessToken();
return {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json'
};
}
}

0 comments on commit d60ef31

Please sign in to comment.