Skip to content

Commit f498747

Browse files
add tableau delta sharing connector for sandbox project (#355)
Adding the tableau delta sharing oauth connector to databricks labs sandbox project, so that customer can try this connector on their own.
1 parent 7e0a1a1 commit f498747

16 files changed

+53425
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# logs
2+
log
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
lerna-debug.log*
8+
.pnpm-debug.log*
9+
10+
# taco file - generated WDC
11+
*.taco
12+
13+
# Dependency directories
14+
node_modules/
15+
16+
# build output
17+
dist
18+
19+
# TypeScript cache
20+
*.tsbuildinfo
21+
22+
# parcel cache
23+
connector/.parcel-cache
24+
25+
# runtime
26+
.eps
27+
28+
# mac
29+
.DS_Store
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# databricks-delta-sharing-connector
2+
Databricks's Delta Sharing WDC 3.0 based Connector
3+
4+
The connector is built with the Tableau Web Data Connector 3.0 SDK and provides:
5+
- Share/Schema/Table browsing wihtin a share
6+
- OAuth Authentication
7+
8+
## Prerequisite
9+
- [Python 3.7 or higher](https://www.python.org/downloads/)
10+
- [JDK 11 or higher](https://www.oracle.com/java/technologies/downloads/)
11+
- [Tableau Desktop 2024.1 or later](https://www.tableau.com/support/releases/desktop/2024.1)
12+
- Install [taco-toolkit](https://help.tableau.com/current/api/webdataconnector/en-us/index.html): `npm install -g @tableau/[email protected]`
13+
14+
## Local Test
15+
16+
After cloning and installing npm packages, in the top level directory:
17+
18+
To compile/build project
19+
```taco build```
20+
21+
To produce .taco file (for Tableau Desktop testing)
22+
```taco pack```
23+
24+
To run .taco file in top level directory (launches Tableau Desktop, runs interactive phase + data gathering phase)
25+
```taco run Desktop```
26+
27+
The current connector.json file has an example OAuth setting which target EntraId with your default/home tenant.
28+
If you are using EntraID, you only need to change the tenentId from **common** to your tenantId in the **authUri**/**tokenUri**.
29+
If you are using other Idp you need to follow your Idp guidence of how to register an app and grab the folloing fields:
30+
- **clientIdDesktop**: The OAuth ClientId target your IDP
31+
- **authUri**: The authentication url for your IDP
32+
- **tokenUri**: The token url for your IDP
33+
- **scopes**: The OAuth scopes to use with your IDP
34+
35+
E.g. for Okta: https://developer.okta.com/docs/guides/implement-oauth-for-okta/main/.
36+
37+
## Run in Tableau
38+
Please refer to [Tableau doc](https://tableau.github.io/connector-plugin-sdk/docs/run-taco)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<connection-fields>
3+
<field default-value="delta_sharing_oauth" editable="false" value-type="string" category="endpoint" label="@string/server_prompt/" name="server"/>
4+
<field default-value="oauth" editable="false" value-type="string" category="authentication" label="Authentication" name="authentication"/>
5+
<field default-value="12082020" editable="false" value-type="string" category="general" label="eps-alpha-version" name="eps-alpha-version"/>
6+
<field default-value="" editable="false" value-type="string" category="general" label="connectionData" name="connectionData"/>
7+
<field default-value="true" editable="false" value-type="string" category="general" label="is-eps-taco" name="is-eps-taco"/>
8+
<field default-value="" editable="false" value-type="string" category="general" label="eps-connection-id" name="eps-connection-id"/>
9+
</connection-fields>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<connection-metadata>
3+
<database enabled='false' label="@string/http_path_prompt/" />
4+
<schema enabled='false' label="@string/metadata_database_prompt/"/>
5+
<table enabled='true' label="@string/metadata_table_prompt/"/>
6+
</connection-metadata>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import ReactDOM from 'react-dom/client'
2+
import React from 'react'
3+
import 'bootstrap'
4+
import ConnectorView from './components/ConnectorView'
5+
6+
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
7+
root.render(
8+
<React.StrictMode>
9+
<ConnectorView />
10+
</React.StrictMode>
11+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
.box {
2+
display: block;
3+
align-items: center;
4+
justify-content: center;
5+
height: 20vh;
6+
}
7+
8+
.error {
9+
display: block;
10+
margin-top: 2em;
11+
height: 5px;
12+
text-align: center;
13+
color: red;
14+
}
15+
16+
.card {
17+
margin: 20px;
18+
}
19+
20+
.card-body {
21+
overflow: auto;
22+
}
23+
24+
.browser-card {
25+
height: 400px;
26+
}
27+
28+
/* custom css for horizontal 'or' divider */
29+
.or {
30+
display:flex;
31+
justify-content:center;
32+
align-items: center;
33+
color:grey;
34+
}
35+
.or:after,
36+
.or:before {
37+
content: "";
38+
display: block;
39+
background: grey;
40+
width: 30%;
41+
height:1px;
42+
margin: 0 10px;
43+
}
44+
45+
/* custom css to change bootstrap's file upload text */
46+
.custom-file-button input[type=file] {
47+
margin-left: -2px !important;
48+
}
49+
.custom-file-button input[type=file]::-webkit-file-upload-button {
50+
display: none;
51+
}
52+
.custom-file-button input[type=file]::file-selector-button {
53+
display: none;
54+
}
55+
.custom-file-button:hover label {
56+
background-color: #dde0e3;
57+
cursor: pointer;
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import 'react-checkbox-tree/lib/react-checkbox-tree.css';
2+
import 'font-awesome/css/font-awesome.min.css';
3+
import React, { useState, useEffect } from 'react'
4+
import CheckboxTree from 'react-checkbox-tree'
5+
import useConnector from './useConnector'
6+
import { Logger } from '@tableau/taco-toolkit';
7+
8+
9+
10+
const ConnectorView = () => {
11+
const {
12+
isSubmitting,
13+
errorMessage,
14+
isInitializing,
15+
hasCreds,
16+
deltaShareStructure,
17+
oauthCredentials,
18+
19+
handleSubmit,
20+
handleCreds,
21+
} = useConnector()
22+
const [creds, setCreds] = useState({
23+
endpoint: '',
24+
bearerToken: '',
25+
})
26+
const [sqlFilters, setSqlFilters] = useState([] as string[])
27+
const [rowLimit, setRowLimit] = useState('')
28+
const [checked, setChecked] = useState([] as string[])
29+
const [expanded, setExpanded] = useState([] as string[])
30+
31+
const credsInputHandler = (e: React.FormEvent<HTMLInputElement>) => {
32+
const { name, value } = e.currentTarget;
33+
setCreds({
34+
endpoint: creds.endpoint,
35+
bearerToken: creds.bearerToken,
36+
[name]: value
37+
})
38+
}
39+
const sqlInputHandler = (e: React.FormEvent<HTMLInputElement>) => {
40+
setSqlFilters([e.currentTarget.value])
41+
}
42+
const shareFileInputHandler = (e: React.FormEvent<HTMLInputElement>) => {
43+
const file = e.currentTarget.files ? e.currentTarget.files[0] : null
44+
if (!file) {
45+
return
46+
}
47+
const read = new FileReader()
48+
read.onloadend = () => {
49+
const data = read.result?.toString()
50+
const creds = data ? JSON.parse(data) : null
51+
if (creds) {
52+
setCreds(creds)
53+
}
54+
}
55+
read.readAsText(file)
56+
}
57+
const rowLimitInputHandler = (e: React.FormEvent<HTMLInputElement>) => {
58+
// regex test for numbers only
59+
const re = /^[0-9\b]+$/
60+
// if not blank, test regex
61+
if (e.currentTarget.value === '' || re.test(e.currentTarget.value)) {
62+
setRowLimit(e.currentTarget.value)
63+
}
64+
}
65+
66+
const onSubmit = () => {
67+
handleSubmit(checked, sqlFilters, rowLimit)
68+
}
69+
const onSendCreds = () => {
70+
handleCreds(creds.endpoint, creds.bearerToken);
71+
72+
}
73+
74+
const uniqueSelection = (selected: string[]) => {
75+
const diff = selected.filter(x => !checked.includes(x))
76+
setChecked(diff)
77+
}
78+
79+
// UseEffect to set creds.token to accessToken when available
80+
useEffect(() => {
81+
if (oauthCredentials?.accessToken) {
82+
setCreds((prevCreds) => ({
83+
...prevCreds,
84+
bearerToken: oauthCredentials.accessToken ?? '', // Automatically set accessToken to creds.token
85+
}));
86+
}
87+
}, [oauthCredentials]);
88+
89+
if (isInitializing) {
90+
return <div className="p-3 text-muted text-center">Initializing...</div>
91+
}
92+
93+
if (!hasCreds) {
94+
return (
95+
<>
96+
<div className="box m-auto">
97+
<div className="card">
98+
<div className="card-header">
99+
Sharing Server Credentials
100+
</div>
101+
102+
<form className="card-body">
103+
104+
<label htmlFor="endpoint" className="form-label">Endpoint URL</label>
105+
<input key="endpoint" name="endpoint" onChange={credsInputHandler} value={creds.endpoint} className="form-control mb-2" placeholder="https://sharing.delta.io/delta-sharing"/>
106+
107+
108+
<label htmlFor="bearerToken" className="form-label">Bearer Token</label>
109+
<input key="bearerToken" name="bearerToken" onChange={credsInputHandler} value={creds.bearerToken} className="form-control mb-3" placeholder="" type="password"/>
110+
111+
<div className="or mb-3"> or </div>
112+
113+
<div className="input-group custom-file-button">
114+
<label className="input-group-text" htmlFor="inputGroupFile">Upload share file</label>
115+
<input type="file" className="form-control" id="inputGroupFile" accept=".share" onChange={shareFileInputHandler}/>
116+
</div>
117+
118+
</form>
119+
</div>
120+
<div className=" text-center">
121+
<button type="button" className="btn btn-success" onClick={onSendCreds} disabled={isSubmitting}>
122+
{ isInitializing ? 'Initializing...' : (isSubmitting ? 'Loading tables...' : 'Get Data') }
123+
</button>
124+
</div>
125+
{errorMessage && <div className="alert alert-danger">{errorMessage}</div>}
126+
127+
</div>
128+
</>
129+
)
130+
}
131+
132+
return (
133+
<>
134+
<div className="box m-auto">
135+
<div className="card browser-card">
136+
<div className="card-header">
137+
Data Explorer
138+
</div>
139+
<div className="card-body">
140+
{
141+
deltaShareStructure ?
142+
<CheckboxTree
143+
nodes={deltaShareStructure}
144+
checked={checked}
145+
expanded={expanded}
146+
onlyLeafCheckboxes={true}
147+
expandOnClick={true}
148+
onCheck={uniqueSelection}
149+
onExpand={expanded => setExpanded(expanded)}
150+
iconsClass="fa5"
151+
/>
152+
:
153+
<div className="d-flex justify-content-center">
154+
<div className="spinner-border" role="status">
155+
<span className="sr-only">Loading...</span>
156+
</div>
157+
</div>
158+
}
159+
160+
</div>
161+
</div>
162+
163+
<div className="card">
164+
<div className="card-header">
165+
Filtering
166+
</div>
167+
<form className="card-body">
168+
<label htmlFor="sqlFilter" className="form-label">SQL Filter</label>
169+
<input key="sqlFilter" onChange={sqlInputHandler} value={sqlFilters[0]} className="form-control mb-2" placeholder="eg. date >= '2021-01-01' AND magnitude <= 5.3"/>
170+
171+
<label htmlFor="rowFilter" className="form-label">Row Limit</label>
172+
<input key="rowFilter" onChange={rowLimitInputHandler} value={rowLimit} className="form-control mb-3" placeholder="eg. 234"/>
173+
</form>
174+
</div>
175+
176+
<div className="text-center">
177+
<button type="button" className="btn btn-success" onClick={onSubmit} disabled={isSubmitting || checked.length === 0}>
178+
Get Table Data
179+
</button>
180+
</div>
181+
</div>
182+
</>
183+
)
184+
}
185+
186+
export default ConnectorView

0 commit comments

Comments
 (0)