@pipedream/tableau 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # Overview
2
+
3
+ The Tableau API allows you to tap into the robust data visualization and business intelligence capabilities of Tableau. Within Pipedream, you can leverage this API to automate reporting, manage users, update data sources, and extract insights. This enables you to integrate Tableau's analytics with other services, streamlining your data workflows and ensuring your dashboards remain up-to-date with minimal manual effort.
4
+
5
+ # Example Use Cases
6
+
7
+ - **Automated Snapshot Sharing**: Generate snapshots of your key Tableau dashboards and share them via email or Slack at regular intervals. This keeps your team informed with the latest business insights without manual exports.
8
+
9
+ _Example Workflow_: Trigger a Pipedream workflow on a schedule; use Tableau API to capture a view of the dashboard; send the image via Gmail or post to a Slack channel using their respective Pipedream app integrations.
10
+
11
+ - **Dynamic Data Updates**: Automatically update Tableau data sources when new data comes into your backend systems, ensuring that Tableau dashboards reflect the most current data.
12
+
13
+ _Example Workflow_: Trigger a Pipedream workflow with a webhook when new data is added to a database; process and format the data within the workflow; use Tableau API to refresh the corresponding data source on Tableau Server or Tableau Online.
14
+
15
+ - **User Management Automation**: Streamline user provisioning by automating the addition and removal of users to and from Tableau sites based on HR software triggers or internal databases.
16
+
17
+ _Example Workflow_: Trigger a Pipedream workflow from a user management event in an app like BambooHR; use conditions within the workflow to determine if the user should be added or removed; utilize Tableau API to update the user list on the relevant Tableau site.
@@ -0,0 +1,72 @@
1
+ import app from "../../tableau.app.mjs";
2
+
3
+ export default {
4
+ key: "tableau-create-project",
5
+ name: "Create Project",
6
+ description: "Creates a project on the specified site. You can also create project hierarchies by creating a project under the specified parent project on the site. [See the documentation](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_projects.htm#create_project)",
7
+ version: "0.0.2",
8
+ type: "action",
9
+ props: {
10
+ app,
11
+ siteId: {
12
+ propDefinition: [
13
+ app,
14
+ "siteId",
15
+ ],
16
+ },
17
+ name: {
18
+ type: "string",
19
+ label: "Project Name",
20
+ description: "The name of the new project to create",
21
+ },
22
+ description: {
23
+ type: "string",
24
+ label: "Project Description",
25
+ description: "The description of the new project to create",
26
+ optional: true,
27
+ },
28
+ parentProjectId: {
29
+ propDefinition: [
30
+ app,
31
+ "parentProjectId",
32
+ ({ siteId }) => ({
33
+ siteId,
34
+ }),
35
+ ],
36
+ },
37
+ },
38
+ methods: {
39
+ createProject({
40
+ siteId, ...args
41
+ } = {}) {
42
+ return this.app.post({
43
+ path: `/sites/${siteId}/projects`,
44
+ ...args,
45
+ });
46
+ },
47
+ },
48
+ async run({ $ }) {
49
+ const {
50
+ createProject,
51
+ siteId,
52
+ name,
53
+ description,
54
+ parentProjectId,
55
+ } = this;
56
+
57
+ const response = await createProject({
58
+ $,
59
+ siteId,
60
+ data: {
61
+ project: {
62
+ name,
63
+ description,
64
+ parentProjectId,
65
+ },
66
+ },
67
+ });
68
+
69
+ $.export("$summary", `Successfully created project with ID \`${response.project?.id}\``);
70
+ return response;
71
+ },
72
+ };
@@ -0,0 +1,130 @@
1
+ import app from "../../tableau.app.mjs";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ export default {
6
+ key: "tableau-download-pdf",
7
+ name: "Download PDF",
8
+ description: "Downloads images of the sheets of a workbook as a PDF file. [See the documentation](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#download_workbook_pdf)",
9
+ version: "0.0.1",
10
+ type: "action",
11
+ props: {
12
+ app,
13
+ siteId: {
14
+ propDefinition: [
15
+ app,
16
+ "siteId",
17
+ ],
18
+ },
19
+ workbookId: {
20
+ propDefinition: [
21
+ app,
22
+ "workbookId",
23
+ ({ siteId }) => ({
24
+ siteId,
25
+ }),
26
+ ],
27
+ },
28
+ maxAge: {
29
+ type: "integer",
30
+ label: "Max Age",
31
+ description: "The maximum number of minutes a workbook PDF will be cached before being refreshed. Minimum interval is one minute",
32
+ optional: true,
33
+ min: 1,
34
+ },
35
+ orientation: {
36
+ type: "string",
37
+ label: "Page Orientation",
38
+ description: "The orientation of the pages in the PDF file produced",
39
+ options: [
40
+ "Portrait",
41
+ "Landscape",
42
+ ],
43
+ default: "Portrait",
44
+ optional: true,
45
+ },
46
+ pageType: {
47
+ type: "string",
48
+ label: "Page Type",
49
+ description: "The type of page, which determines the page dimensions of the PDF file returned",
50
+ options: [
51
+ "A3",
52
+ "A4",
53
+ "A5",
54
+ "B5",
55
+ "Executive",
56
+ "Folio",
57
+ "Ledger",
58
+ "Legal",
59
+ "Letter",
60
+ "Note",
61
+ "Quarto",
62
+ "Tabloid",
63
+ ],
64
+ default: "Legal",
65
+ optional: true,
66
+ },
67
+ vizHeight: {
68
+ type: "integer",
69
+ label: "Viz Height",
70
+ description: "The height of the rendered PDF image in pixels that, along with `Viz Width`, determines its resolution and aspect ratio",
71
+ optional: true,
72
+ },
73
+ vizWidth: {
74
+ type: "integer",
75
+ label: "Viz Width",
76
+ description: "The width of the rendered PDF image in pixels that, along with `Viz Height`, determines its resolution and aspect ratio",
77
+ optional: true,
78
+ },
79
+ outputFilename: {
80
+ type: "string",
81
+ label: "Output Filename",
82
+ description: "The filename for the downloaded PDF file, which will be saved to the `/tmp` folder",
83
+ default: "workbook.pdf",
84
+ optional: true,
85
+ },
86
+ syncDir: {
87
+ type: "dir",
88
+ accessMode: "write",
89
+ sync: true,
90
+ },
91
+ },
92
+ async run({ $ }) {
93
+ const {
94
+ siteId,
95
+ workbookId,
96
+ maxAge,
97
+ orientation,
98
+ pageType,
99
+ vizHeight,
100
+ vizWidth,
101
+ outputFilename,
102
+ } = this;
103
+
104
+ const response = await this.app.downloadWorkbookPdf({
105
+ $,
106
+ siteId,
107
+ workbookId,
108
+ params: {
109
+ "max-age-minutes": maxAge,
110
+ orientation,
111
+ "page-type": pageType,
112
+ "viz-height": vizHeight,
113
+ "viz-width": vizWidth,
114
+ },
115
+ responseType: "arraybuffer",
116
+ });
117
+
118
+ // Write the PDF to the /tmp folder
119
+ const filename = outputFilename || "workbook.pdf";
120
+ const filePath = path.join("/tmp", filename);
121
+
122
+ await fs.promises.writeFile(filePath, Buffer.from(response));
123
+
124
+ $.export("$summary", `Successfully downloaded workbook PDF to \`${filePath}\``);
125
+ return {
126
+ filePath,
127
+ fileContent: response,
128
+ };
129
+ },
130
+ };
@@ -0,0 +1,40 @@
1
+ import app from "../../tableau.app.mjs";
2
+ import constants from "../../common/constants.mjs";
3
+
4
+ export default {
5
+ key: "tableau-query-projects",
6
+ name: "Query Projects",
7
+ description: "Returns a list of projects on the specified site. [See the documentation](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_projects.htm)",
8
+ version: "0.0.2",
9
+ type: "action",
10
+ props: {
11
+ app,
12
+ siteId: {
13
+ propDefinition: [
14
+ app,
15
+ "siteId",
16
+ ],
17
+ },
18
+ },
19
+ async run({ $ }) {
20
+ const {
21
+ app,
22
+ siteId,
23
+ } = this;
24
+
25
+ const projects = await app.paginate({
26
+ resourcesFn: app.listProjects,
27
+ resourcesFnArgs: {
28
+ $,
29
+ siteId,
30
+ params: {
31
+ pageSize: constants.DEFAULT_LIMIT,
32
+ },
33
+ },
34
+ resourceName: "projects.project",
35
+ });
36
+
37
+ $.export("$summary", `Successfully retrieved \`${projects.length}\` project(s)`);
38
+ return projects;
39
+ },
40
+ };
@@ -0,0 +1,11 @@
1
+ const VERSION_PATH = "/api/3.21";
2
+ const DEFAULT_MAX = 600;
3
+ const DEFAULT_LIMIT = 100;
4
+ const WEBHOOK_ID = "webhookId";
5
+
6
+ export default {
7
+ VERSION_PATH,
8
+ DEFAULT_MAX,
9
+ DEFAULT_LIMIT,
10
+ WEBHOOK_ID,
11
+ };
@@ -0,0 +1,17 @@
1
+ async function iterate(iterations) {
2
+ const items = [];
3
+ for await (const item of iterations) {
4
+ items.push(item);
5
+ }
6
+ return items;
7
+ }
8
+
9
+ function getNestedProperty(obj, propertyString) {
10
+ const properties = propertyString.split(".");
11
+ return properties.reduce((prev, curr) => prev && prev[curr], obj);
12
+ }
13
+
14
+ export default {
15
+ iterate,
16
+ getNestedProperty,
17
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipedream/tableau",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "description": "Pipedream Tableau Components",
5
5
  "main": "tableau.app.mjs",
6
6
  "keywords": [
@@ -11,5 +11,8 @@
11
11
  "author": "Pipedream <support@pipedream.com> (https://pipedream.com/)",
12
12
  "publishConfig": {
13
13
  "access": "public"
14
+ },
15
+ "dependencies": {
16
+ "@pipedream/platform": "^1.6.0"
14
17
  }
15
18
  }
@@ -0,0 +1,24 @@
1
+ export default {
2
+ ADMIN_PROMOTED: "AdminPromoted",
3
+ ADMIN_DEMOTED: "AdminDemoted",
4
+ DATASOURCE_UPDATED: "DatasourceUpdated",
5
+ DATASOURCE_CREATED: "DatasourceCreated",
6
+ DATASOURCE_DELETED: "DatasourceDeleted",
7
+ DATASOURCE_REFRESH_STARTED: "DatasourceRefreshStarted",
8
+ DATASOURCE_REFRESH_SUCCEEDED: "DatasourceRefreshSucceeded",
9
+ DATASOURCE_REFRESH_FAILED: "DatasourceRefreshFailed",
10
+ LABEL_CREATED: "LabelCreated",
11
+ LABEL_UPDATED: "LabelUpdated",
12
+ LABEL_DELETED: "LabelDeleted",
13
+ SITE_CREATED: "SiteCreated",
14
+ SITE_UPDATED: "SiteUpdated",
15
+ SITE_DELETED: "SiteDeleted",
16
+ USER_DELETED: "UserDeleted",
17
+ VIEW_DELETED: "ViewDeleted",
18
+ WORKBOOK_UPDATED: "WorkbookUpdated",
19
+ WORKBOOK_CREATED: "WorkbookCreated",
20
+ WORKBOOK_DELETED: "WorkbookDeleted",
21
+ WORKBOOK_REFRESH_STARTED: "WorkbookRefreshStarted",
22
+ WORKBOOK_REFRESH_SUCCEEDED: "WorkbookRefreshSucceeded",
23
+ WORKBOOK_REFRESH_FAILED: "WorkbookRefreshFailed",
24
+ };
@@ -0,0 +1,105 @@
1
+ import { ConfigurationError } from "@pipedream/platform";
2
+ import app from "../../tableau.app.mjs";
3
+ import constants from "../../common/constants.mjs";
4
+
5
+ export default {
6
+ props: {
7
+ app,
8
+ db: "$.service.db",
9
+ http: "$.interface.http",
10
+ siteId: {
11
+ propDefinition: [
12
+ app,
13
+ "siteId",
14
+ ],
15
+ },
16
+ },
17
+ hooks: {
18
+ async activate() {
19
+ const {
20
+ createWebhook,
21
+ siteId,
22
+ getWebhookName,
23
+ getEventName,
24
+ http,
25
+ setWebhookId,
26
+ } = this;
27
+
28
+ const { webhook: { id: webhookId } } =
29
+ await createWebhook({
30
+ siteId,
31
+ data: {
32
+ webhook: {
33
+ "name": getWebhookName(),
34
+ "event": getEventName(),
35
+ "isEnabled": true,
36
+ "webhook-destination": {
37
+ "webhook-destination-http": {
38
+ "method": "POST",
39
+ "url": http.endpoint,
40
+ },
41
+ },
42
+ },
43
+ },
44
+ });
45
+
46
+ setWebhookId(webhookId);
47
+ },
48
+ async deactivate() {
49
+ const {
50
+ getWebhookId,
51
+ siteId,
52
+ deleteWebhook,
53
+ } = this;
54
+
55
+ const webhookId = getWebhookId();
56
+ if (webhookId) {
57
+ await deleteWebhook({
58
+ siteId,
59
+ webhookId,
60
+ });
61
+ }
62
+ },
63
+ },
64
+ methods: {
65
+ setWebhookId(value) {
66
+ this.db.set(constants.WEBHOOK_ID, value);
67
+ },
68
+ getWebhookId() {
69
+ return this.db.get(constants.WEBHOOK_ID);
70
+ },
71
+ generateMeta() {
72
+ throw new ConfigurationError("generateMeta is not implemented");
73
+ },
74
+ getWebhookName() {
75
+ throw new ConfigurationError("getWebhookName is not implemented");
76
+ },
77
+ getEventName() {
78
+ throw new ConfigurationError("getEventName is not implemented");
79
+ },
80
+ processResource(resource) {
81
+ this.$emit(resource, this.generateMeta(resource));
82
+ },
83
+ createWebhook({
84
+ siteId, ...args
85
+ } = {}) {
86
+ return this.app.post({
87
+ debug: true,
88
+ path: `/sites/${siteId}/webhooks`,
89
+ ...args,
90
+ });
91
+ },
92
+ deleteWebhook({
93
+ siteId, webhookId, ...args
94
+ } = {}) {
95
+ return this.app.delete({
96
+ debug: true,
97
+ path: `/sites/${siteId}/webhooks/${webhookId}`,
98
+ ...args,
99
+ });
100
+ },
101
+ },
102
+ run({ body }) {
103
+ this.processResource(body);
104
+ },
105
+ };
@@ -0,0 +1,28 @@
1
+ import common from "../common/webhook.mjs";
2
+ import events from "../common/events.mjs";
3
+
4
+ export default {
5
+ ...common,
6
+ key: "tableau-label-created-instant",
7
+ name: "New Label Created (Instant)",
8
+ description: "Emit new event when a label is created in Tableau. [See the documentation](https://help.tableau.com/current/developer/webhooks/en-us/docs/webhooks-events-payload.html)",
9
+ version: "0.0.2",
10
+ type: "source",
11
+ dedupe: "unique",
12
+ methods: {
13
+ ...common.methods,
14
+ getWebhookName() {
15
+ return "label-creation";
16
+ },
17
+ getEventName() {
18
+ return events.LABEL_CREATED;
19
+ },
20
+ generateMeta(resource) {
21
+ return {
22
+ id: resource.resource_luid,
23
+ summary: `New Label: ${resource.resource_luid}`,
24
+ ts: Date.parse(resource.created_at),
25
+ };
26
+ },
27
+ },
28
+ };
@@ -0,0 +1,28 @@
1
+ import common from "../common/webhook.mjs";
2
+ import events from "../common/events.mjs";
3
+
4
+ export default {
5
+ ...common,
6
+ key: "tableau-workbook-created-instant",
7
+ name: "New Workbook Created (Instant)",
8
+ description: "Emit new event each time a new workbook is created in Tableau. [See the documentation](https://help.tableau.com/current/developer/webhooks/en-us/docs/webhooks-events-payload.html)",
9
+ version: "0.0.2",
10
+ type: "source",
11
+ dedupe: "unique",
12
+ methods: {
13
+ ...common.methods,
14
+ getWebhookName() {
15
+ return "workbook-creation";
16
+ },
17
+ getEventName() {
18
+ return events.WORKBOOK_CREATED;
19
+ },
20
+ generateMeta(resource) {
21
+ return {
22
+ id: resource.resource_luid,
23
+ summary: `New Workbook: ${resource.resource_luid}`,
24
+ ts: Date.parse(resource.created_at),
25
+ };
26
+ },
27
+ },
28
+ };
package/tableau.app.mjs CHANGED
@@ -1,11 +1,187 @@
1
+ import { axios } from "@pipedream/platform";
2
+ import utils from "./common/utils.mjs";
3
+ import constants from "./common/constants.mjs";
4
+
1
5
  export default {
2
6
  type: "app",
3
7
  app: "tableau",
4
- propDefinitions: {},
8
+ propDefinitions: {
9
+ siteId: {
10
+ type: "string",
11
+ label: "Site ID",
12
+ description: "The ID of the site where the project or event is located",
13
+ async options() {
14
+ const { session: { site } } = await this.getCurrentSession();
15
+ return [
16
+ {
17
+ label: site.name,
18
+ value: site.id,
19
+ },
20
+ ];
21
+ },
22
+ },
23
+ parentProjectId: {
24
+ type: "string",
25
+ label: "Parent Project ID",
26
+ description: "The ID of the parent project under which the new project will be created",
27
+ optional: true,
28
+ async options({
29
+ siteId, page,
30
+ }) {
31
+ if (!siteId) {
32
+ return [];
33
+ }
34
+ const { projects: { project: data } } =
35
+ await this.listProjects({
36
+ siteId,
37
+ params: {
38
+ pageSize: constants.DEFAULT_LIMIT,
39
+ pageNumber: page + 1,
40
+ },
41
+ });
42
+ return data.map(({
43
+ id: value, name: label,
44
+ }) => ({
45
+ label,
46
+ value,
47
+ }));
48
+ },
49
+ },
50
+ workbookId: {
51
+ type: "string",
52
+ label: "Workbook ID",
53
+ description: "The ID of the workbook to download as PDF",
54
+ async options({
55
+ siteId, page,
56
+ }) {
57
+ if (!siteId) {
58
+ return [];
59
+ }
60
+ const { workbooks: { workbook: data } } =
61
+ await this.listWorkbooks({
62
+ siteId,
63
+ params: {
64
+ pageSize: constants.DEFAULT_LIMIT,
65
+ pageNumber: page + 1,
66
+ },
67
+ });
68
+ return data?.map(({
69
+ id: value, name: label,
70
+ }) => ({
71
+ label,
72
+ value,
73
+ })) || [];
74
+ },
75
+ },
76
+ },
5
77
  methods: {
6
- // this.$auth contains connected account data
7
- authKeys() {
8
- console.log(Object.keys(this.$auth));
78
+ getUrl(path) {
79
+ return `https://${this.$auth.domain}${constants.VERSION_PATH}${path}`;
80
+ },
81
+ getHeaders(headers) {
82
+ return {
83
+ ...headers,
84
+ "Accept": "application/json",
85
+ "Content-Type": "application/json",
86
+ "X-Tableau-Auth": this.$auth.oauth_access_token,
87
+ };
88
+ },
89
+ _makeRequest({
90
+ $ = this, path, headers, ...args
91
+ } = {}) {
92
+ return axios($, {
93
+ ...args,
94
+ url: this.getUrl(path),
95
+ headers: this.getHeaders(headers),
96
+ });
97
+ },
98
+ post(args = {}) {
99
+ return this._makeRequest({
100
+ method: "POST",
101
+ ...args,
102
+ });
103
+ },
104
+ delete(args = {}) {
105
+ return this._makeRequest({
106
+ method: "DELETE",
107
+ ...args,
108
+ });
109
+ },
110
+ getCurrentSession(args = {}) {
111
+ return this._makeRequest({
112
+ path: "/sessions/current",
113
+ ...args,
114
+ });
115
+ },
116
+ listProjects({
117
+ siteId, ...args
118
+ }) {
119
+ return this._makeRequest({
120
+ path: `/sites/${siteId}/projects`,
121
+ ...args,
122
+ });
123
+ },
124
+ listWorkbooks({
125
+ siteId, ...args
126
+ }) {
127
+ return this._makeRequest({
128
+ path: `/sites/${siteId}/workbooks`,
129
+ ...args,
130
+ });
131
+ },
132
+ downloadWorkbookPdf({
133
+ siteId, workbookId, ...args
134
+ }) {
135
+ return this._makeRequest({
136
+ path: `/sites/${siteId}/workbooks/${workbookId}/pdf`,
137
+ ...args,
138
+ });
139
+ },
140
+ async *getIterations({
141
+ resourcesFn, resourcesFnArgs, resourceName,
142
+ max = constants.DEFAULT_MAX,
143
+ }) {
144
+ let pageNumber = 1;
145
+ let resourcesCount = 0;
146
+
147
+ while (true) {
148
+ const response =
149
+ await resourcesFn({
150
+ ...resourcesFnArgs,
151
+ params: {
152
+ ...resourcesFnArgs?.params,
153
+ pageNumber,
154
+ },
155
+ });
156
+
157
+ const nextResources = utils.getNestedProperty(response, resourceName);
158
+
159
+ if (!nextResources?.length) {
160
+ console.log("No more resources found");
161
+ return;
162
+ }
163
+
164
+ for (const resource of nextResources) {
165
+ yield resource;
166
+ resourcesCount += 1;
167
+
168
+ if (resourcesCount >= max) {
169
+ return;
170
+ }
171
+ }
172
+
173
+ const totalAvailable = response.pagination?.totalAvailable;
174
+
175
+ if (resourcesCount >= totalAvailable) {
176
+ console.log("There are no more resources to fetch");
177
+ return;
178
+ }
179
+
180
+ pageNumber += 1;
181
+ }
182
+ },
183
+ paginate(args = {}) {
184
+ return utils.iterate(this.getIterations(args));
9
185
  },
10
186
  },
11
- };
187
+ };