@slates-integrations/tableau 0.2.0-rc.8
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 +85 -0
- package/docs/SPEC.md +61 -0
- package/logo.png +0 -0
- package/package.json +19 -0
- package/slate.json +18 -0
- package/src/auth.ts +237 -0
- package/src/config.ts +17 -0
- package/src/index.ts +55 -0
- package/src/lib/client.ts +959 -0
- package/src/lib/errors.ts +94 -0
- package/src/lib/helpers.ts +15 -0
- package/src/lib/normalizers.ts +9 -0
- package/src/spec.ts +12 -0
- package/src/tools/export-view.ts +112 -0
- package/src/tools/get-site-info.ts +47 -0
- package/src/tools/get-view-data.ts +31 -0
- package/src/tools/index.ts +18 -0
- package/src/tools/list-datasources.ts +78 -0
- package/src/tools/list-views.ts +70 -0
- package/src/tools/list-workbooks.ts +88 -0
- package/src/tools/manage-alerts.ts +139 -0
- package/src/tools/manage-collections.ts +254 -0
- package/src/tools/manage-custom-views.ts +159 -0
- package/src/tools/manage-datasource.ts +129 -0
- package/src/tools/manage-favorites.ts +80 -0
- package/src/tools/manage-flows.ts +170 -0
- package/src/tools/manage-groups.ts +178 -0
- package/src/tools/manage-jobs.ts +120 -0
- package/src/tools/manage-permissions.ts +118 -0
- package/src/tools/manage-projects.ts +162 -0
- package/src/tools/manage-users.ts +184 -0
- package/src/tools/manage-workbook.ts +160 -0
- package/src/triggers/datasource-events.ts +119 -0
- package/src/triggers/index.ts +6 -0
- package/src/triggers/label-events.ts +98 -0
- package/src/triggers/site-events.ts +97 -0
- package/src/triggers/user-events.ts +98 -0
- package/src/triggers/view-events.ts +83 -0
- package/src/triggers/workbook-events.ts +108 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { createClient } from '../lib/helpers';
|
|
5
|
+
import { tableauServiceError } from '../lib/errors';
|
|
6
|
+
|
|
7
|
+
export let manageFavorites = SlateTool.create(spec, {
|
|
8
|
+
name: 'Manage Favorites',
|
|
9
|
+
key: 'manage_favorites',
|
|
10
|
+
description: `List, add, or remove favorites for a user. Supports workbooks, views, data sources, projects, and flows.`
|
|
11
|
+
})
|
|
12
|
+
.input(
|
|
13
|
+
z.object({
|
|
14
|
+
action: z.enum(['list', 'add', 'remove']).describe('Operation to perform'),
|
|
15
|
+
userId: z.string().describe('User LUID'),
|
|
16
|
+
resourceType: z
|
|
17
|
+
.enum(['workbook', 'view', 'datasource', 'project', 'flow'])
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('Type of resource (for add/remove)'),
|
|
20
|
+
resourceId: z.string().optional().describe('LUID of the resource (for add/remove)'),
|
|
21
|
+
label: z.string().optional().describe('Label for the favorite (for add)')
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
.output(
|
|
25
|
+
z.object({
|
|
26
|
+
favorites: z.any().optional().describe('User favorites list'),
|
|
27
|
+
added: z.boolean().optional(),
|
|
28
|
+
removed: z.boolean().optional()
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
.handleInvocation(async ctx => {
|
|
32
|
+
let client = createClient(ctx.config, ctx.auth);
|
|
33
|
+
let { action, userId } = ctx.input;
|
|
34
|
+
|
|
35
|
+
if (action === 'list') {
|
|
36
|
+
let result = await client.getFavorites(userId);
|
|
37
|
+
return {
|
|
38
|
+
output: { favorites: result.favorites },
|
|
39
|
+
message: `Retrieved favorites for user \`${userId}\`.`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (action === 'add') {
|
|
44
|
+
if (!ctx.input.resourceType) {
|
|
45
|
+
throw tableauServiceError('resourceType is required for add action.');
|
|
46
|
+
}
|
|
47
|
+
if (!ctx.input.resourceId) {
|
|
48
|
+
throw tableauServiceError('resourceId is required for add action.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await client.addFavorite(
|
|
52
|
+
userId,
|
|
53
|
+
ctx.input.resourceType,
|
|
54
|
+
ctx.input.resourceId,
|
|
55
|
+
ctx.input.label || ctx.input.resourceType
|
|
56
|
+
);
|
|
57
|
+
return {
|
|
58
|
+
output: { added: true },
|
|
59
|
+
message: `Added ${ctx.input.resourceType} \`${ctx.input.resourceId}\` to favorites.`
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (action === 'remove') {
|
|
64
|
+
if (!ctx.input.resourceType) {
|
|
65
|
+
throw tableauServiceError('resourceType is required for remove action.');
|
|
66
|
+
}
|
|
67
|
+
if (!ctx.input.resourceId) {
|
|
68
|
+
throw tableauServiceError('resourceId is required for remove action.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await client.deleteFavorite(userId, ctx.input.resourceType + 's', ctx.input.resourceId);
|
|
72
|
+
return {
|
|
73
|
+
output: { removed: true },
|
|
74
|
+
message: `Removed ${ctx.input.resourceType} \`${ctx.input.resourceId}\` from favorites.`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
throw tableauServiceError(`Unknown action: ${action}`);
|
|
79
|
+
})
|
|
80
|
+
.build();
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { createClient } from '../lib/helpers';
|
|
5
|
+
import { tableauServiceError } from '../lib/errors';
|
|
6
|
+
|
|
7
|
+
export let manageFlows = SlateTool.create(spec, {
|
|
8
|
+
name: 'Manage Flows',
|
|
9
|
+
key: 'manage_flows',
|
|
10
|
+
description: `List, get, update, delete, or run Tableau Prep flows. Use the **action** field to select the operation.`
|
|
11
|
+
})
|
|
12
|
+
.input(
|
|
13
|
+
z.object({
|
|
14
|
+
action: z
|
|
15
|
+
.enum(['list', 'get', 'update', 'delete', 'run'])
|
|
16
|
+
.describe('Operation to perform'),
|
|
17
|
+
flowId: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Flow LUID (required for get, update, delete, run)'),
|
|
21
|
+
name: z.string().optional().describe('New name (for update)'),
|
|
22
|
+
description: z.string().optional().describe('New description (for update)'),
|
|
23
|
+
projectId: z.string().optional().describe('New project LUID (for update)'),
|
|
24
|
+
ownerUserId: z.string().optional().describe('New owner LUID (for update)'),
|
|
25
|
+
pageSize: z.number().optional().describe('Page size for list'),
|
|
26
|
+
pageNumber: z.number().optional().describe('Page number for list'),
|
|
27
|
+
filter: z.string().optional().describe('Filter expression for list'),
|
|
28
|
+
sort: z.string().optional().describe('Sort expression for list')
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
.output(
|
|
32
|
+
z.object({
|
|
33
|
+
flows: z
|
|
34
|
+
.array(
|
|
35
|
+
z.object({
|
|
36
|
+
flowId: z.string(),
|
|
37
|
+
name: z.string().optional(),
|
|
38
|
+
description: z.string().optional(),
|
|
39
|
+
projectId: z.string().optional(),
|
|
40
|
+
projectName: z.string().optional(),
|
|
41
|
+
ownerId: z.string().optional(),
|
|
42
|
+
createdAt: z.string().optional(),
|
|
43
|
+
updatedAt: z.string().optional()
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
.optional(),
|
|
47
|
+
flow: z
|
|
48
|
+
.object({
|
|
49
|
+
flowId: z.string(),
|
|
50
|
+
name: z.string().optional(),
|
|
51
|
+
description: z.string().optional(),
|
|
52
|
+
projectId: z.string().optional(),
|
|
53
|
+
projectName: z.string().optional(),
|
|
54
|
+
ownerId: z.string().optional(),
|
|
55
|
+
createdAt: z.string().optional(),
|
|
56
|
+
updatedAt: z.string().optional()
|
|
57
|
+
})
|
|
58
|
+
.optional(),
|
|
59
|
+
totalCount: z.number().optional(),
|
|
60
|
+
jobId: z.string().optional(),
|
|
61
|
+
deleted: z.boolean().optional()
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
.handleInvocation(async ctx => {
|
|
65
|
+
let client = createClient(ctx.config, ctx.auth);
|
|
66
|
+
let { action } = ctx.input;
|
|
67
|
+
|
|
68
|
+
if (action === 'list') {
|
|
69
|
+
let result = await client.queryFlows({
|
|
70
|
+
pageSize: ctx.input.pageSize,
|
|
71
|
+
pageNumber: ctx.input.pageNumber,
|
|
72
|
+
filter: ctx.input.filter,
|
|
73
|
+
sort: ctx.input.sort
|
|
74
|
+
});
|
|
75
|
+
let pagination = result.pagination || {};
|
|
76
|
+
let flows = (result.flows?.flow || []).map((f: any) => ({
|
|
77
|
+
flowId: f.id,
|
|
78
|
+
name: f.name,
|
|
79
|
+
description: f.description,
|
|
80
|
+
projectId: f.project?.id,
|
|
81
|
+
projectName: f.project?.name,
|
|
82
|
+
ownerId: f.owner?.id,
|
|
83
|
+
createdAt: f.createdAt,
|
|
84
|
+
updatedAt: f.updatedAt
|
|
85
|
+
}));
|
|
86
|
+
return {
|
|
87
|
+
output: { flows, totalCount: Number(pagination.totalAvailable || 0) },
|
|
88
|
+
message: `Found **${flows.length}** flows (${pagination.totalAvailable || 0} total).`
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (action === 'get') {
|
|
93
|
+
if (!ctx.input.flowId) throw tableauServiceError('flowId is required for get action.');
|
|
94
|
+
|
|
95
|
+
let f = await client.getFlow(ctx.input.flowId);
|
|
96
|
+
return {
|
|
97
|
+
output: {
|
|
98
|
+
flow: {
|
|
99
|
+
flowId: f.id,
|
|
100
|
+
name: f.name,
|
|
101
|
+
description: f.description,
|
|
102
|
+
projectId: f.project?.id,
|
|
103
|
+
projectName: f.project?.name,
|
|
104
|
+
ownerId: f.owner?.id,
|
|
105
|
+
createdAt: f.createdAt,
|
|
106
|
+
updatedAt: f.updatedAt
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
message: `Retrieved flow **${f.name}**.`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (action === 'update') {
|
|
114
|
+
if (!ctx.input.flowId)
|
|
115
|
+
throw tableauServiceError('flowId is required for update action.');
|
|
116
|
+
if (
|
|
117
|
+
ctx.input.name === undefined &&
|
|
118
|
+
ctx.input.description === undefined &&
|
|
119
|
+
ctx.input.projectId === undefined &&
|
|
120
|
+
ctx.input.ownerUserId === undefined
|
|
121
|
+
) {
|
|
122
|
+
throw tableauServiceError('Provide at least one field to update a flow.');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let f = await client.updateFlow(ctx.input.flowId, {
|
|
126
|
+
name: ctx.input.name,
|
|
127
|
+
description: ctx.input.description,
|
|
128
|
+
projectId: ctx.input.projectId,
|
|
129
|
+
ownerUserId: ctx.input.ownerUserId
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
output: {
|
|
133
|
+
flow: {
|
|
134
|
+
flowId: f.id,
|
|
135
|
+
name: f.name,
|
|
136
|
+
description: f.description,
|
|
137
|
+
projectId: f.project?.id,
|
|
138
|
+
projectName: f.project?.name,
|
|
139
|
+
ownerId: f.owner?.id,
|
|
140
|
+
updatedAt: f.updatedAt
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
message: `Updated flow **${f.name}**.`
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (action === 'delete') {
|
|
148
|
+
if (!ctx.input.flowId)
|
|
149
|
+
throw tableauServiceError('flowId is required for delete action.');
|
|
150
|
+
|
|
151
|
+
await client.deleteFlow(ctx.input.flowId);
|
|
152
|
+
return {
|
|
153
|
+
output: { deleted: true },
|
|
154
|
+
message: `Deleted flow \`${ctx.input.flowId}\`.`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (action === 'run') {
|
|
159
|
+
if (!ctx.input.flowId) throw tableauServiceError('flowId is required for run action.');
|
|
160
|
+
|
|
161
|
+
let job = await client.runFlow(ctx.input.flowId);
|
|
162
|
+
return {
|
|
163
|
+
output: { jobId: job?.id },
|
|
164
|
+
message: `Started flow run for \`${ctx.input.flowId}\`. Job ID: \`${job?.id}\`.`
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
throw tableauServiceError(`Unknown action: ${action}`);
|
|
169
|
+
})
|
|
170
|
+
.build();
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { createClient } from '../lib/helpers';
|
|
5
|
+
import { tableauServiceError } from '../lib/errors';
|
|
6
|
+
|
|
7
|
+
export let manageGroups = SlateTool.create(spec, {
|
|
8
|
+
name: 'Manage Groups',
|
|
9
|
+
key: 'manage_groups',
|
|
10
|
+
description: `List, create, update, delete groups, and add or remove users from groups. Use the **action** field to select the operation.`,
|
|
11
|
+
tags: { destructive: true }
|
|
12
|
+
})
|
|
13
|
+
.input(
|
|
14
|
+
z.object({
|
|
15
|
+
action: z
|
|
16
|
+
.enum(['list', 'create', 'update', 'delete', 'addUser', 'removeUser', 'listUsers'])
|
|
17
|
+
.describe('Operation to perform'),
|
|
18
|
+
groupId: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('Group LUID (required for update, delete, addUser, removeUser, listUsers)'),
|
|
22
|
+
userId: z.string().optional().describe('User LUID (for addUser, removeUser)'),
|
|
23
|
+
name: z.string().optional().describe('Group name (for create, update)'),
|
|
24
|
+
minimumSiteRole: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Minimum site role for group members (for create, update)'),
|
|
28
|
+
pageSize: z.number().optional().describe('Number of items per page'),
|
|
29
|
+
pageNumber: z.number().optional().describe('Page number (1-based)'),
|
|
30
|
+
filter: z.string().optional().describe('Filter expression for list'),
|
|
31
|
+
sort: z.string().optional().describe('Sort expression for list')
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
.output(
|
|
35
|
+
z.object({
|
|
36
|
+
groups: z
|
|
37
|
+
.array(
|
|
38
|
+
z.object({
|
|
39
|
+
groupId: z.string(),
|
|
40
|
+
name: z.string().optional(),
|
|
41
|
+
minimumSiteRole: z.string().optional()
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
.optional(),
|
|
45
|
+
group: z
|
|
46
|
+
.object({
|
|
47
|
+
groupId: z.string(),
|
|
48
|
+
name: z.string().optional(),
|
|
49
|
+
minimumSiteRole: z.string().optional()
|
|
50
|
+
})
|
|
51
|
+
.optional(),
|
|
52
|
+
users: z
|
|
53
|
+
.array(
|
|
54
|
+
z.object({
|
|
55
|
+
userId: z.string(),
|
|
56
|
+
name: z.string().optional(),
|
|
57
|
+
siteRole: z.string().optional()
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
.optional(),
|
|
61
|
+
totalCount: z.number().optional(),
|
|
62
|
+
deleted: z.boolean().optional(),
|
|
63
|
+
userAdded: z.boolean().optional(),
|
|
64
|
+
userRemoved: z.boolean().optional()
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
.handleInvocation(async ctx => {
|
|
68
|
+
let client = createClient(ctx.config, ctx.auth);
|
|
69
|
+
let { action } = ctx.input;
|
|
70
|
+
|
|
71
|
+
if (action === 'list') {
|
|
72
|
+
let result = await client.queryGroups({
|
|
73
|
+
pageSize: ctx.input.pageSize,
|
|
74
|
+
pageNumber: ctx.input.pageNumber,
|
|
75
|
+
filter: ctx.input.filter,
|
|
76
|
+
sort: ctx.input.sort
|
|
77
|
+
});
|
|
78
|
+
let pagination = result.pagination || {};
|
|
79
|
+
let groups = (result.groups?.group || []).map((g: any) => ({
|
|
80
|
+
groupId: g.id,
|
|
81
|
+
name: g.name,
|
|
82
|
+
minimumSiteRole: g.minimumSiteRole
|
|
83
|
+
}));
|
|
84
|
+
return {
|
|
85
|
+
output: { groups, totalCount: Number(pagination.totalAvailable || 0) },
|
|
86
|
+
message: `Found **${groups.length}** groups (${pagination.totalAvailable || 0} total).`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (action === 'create') {
|
|
91
|
+
if (!ctx.input.name) throw tableauServiceError('name is required for create action.');
|
|
92
|
+
|
|
93
|
+
let g = await client.createGroup(ctx.input.name, ctx.input.minimumSiteRole);
|
|
94
|
+
return {
|
|
95
|
+
output: { group: { groupId: g.id, name: g.name, minimumSiteRole: g.minimumSiteRole } },
|
|
96
|
+
message: `Created group **${g.name}**.`
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (action === 'update') {
|
|
101
|
+
if (!ctx.input.groupId)
|
|
102
|
+
throw tableauServiceError('groupId is required for update action.');
|
|
103
|
+
if (ctx.input.name === undefined && ctx.input.minimumSiteRole === undefined) {
|
|
104
|
+
throw tableauServiceError('Provide name or minimumSiteRole to update a group.');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let g = await client.updateGroup(ctx.input.groupId, {
|
|
108
|
+
name: ctx.input.name,
|
|
109
|
+
minimumSiteRole: ctx.input.minimumSiteRole
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
output: { group: { groupId: g.id, name: g.name, minimumSiteRole: g.minimumSiteRole } },
|
|
113
|
+
message: `Updated group **${g.name}**.`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (action === 'delete') {
|
|
118
|
+
if (!ctx.input.groupId)
|
|
119
|
+
throw tableauServiceError('groupId is required for delete action.');
|
|
120
|
+
|
|
121
|
+
await client.deleteGroup(ctx.input.groupId);
|
|
122
|
+
return {
|
|
123
|
+
output: { deleted: true },
|
|
124
|
+
message: `Deleted group \`${ctx.input.groupId}\`.`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (action === 'addUser') {
|
|
129
|
+
if (!ctx.input.groupId)
|
|
130
|
+
throw tableauServiceError('groupId is required for addUser action.');
|
|
131
|
+
if (!ctx.input.userId)
|
|
132
|
+
throw tableauServiceError('userId is required for addUser action.');
|
|
133
|
+
|
|
134
|
+
await client.addUserToGroup(ctx.input.groupId, ctx.input.userId);
|
|
135
|
+
return {
|
|
136
|
+
output: { userAdded: true },
|
|
137
|
+
message: `Added user \`${ctx.input.userId}\` to group \`${ctx.input.groupId}\`.`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (action === 'removeUser') {
|
|
142
|
+
if (!ctx.input.groupId) {
|
|
143
|
+
throw tableauServiceError('groupId is required for removeUser action.');
|
|
144
|
+
}
|
|
145
|
+
if (!ctx.input.userId)
|
|
146
|
+
throw tableauServiceError('userId is required for removeUser action.');
|
|
147
|
+
|
|
148
|
+
await client.removeUserFromGroup(ctx.input.groupId, ctx.input.userId);
|
|
149
|
+
return {
|
|
150
|
+
output: { userRemoved: true },
|
|
151
|
+
message: `Removed user \`${ctx.input.userId}\` from group \`${ctx.input.groupId}\`.`
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (action === 'listUsers') {
|
|
156
|
+
if (!ctx.input.groupId) {
|
|
157
|
+
throw tableauServiceError('groupId is required for listUsers action.');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let result = await client.getUsersInGroup(ctx.input.groupId, {
|
|
161
|
+
pageSize: ctx.input.pageSize,
|
|
162
|
+
pageNumber: ctx.input.pageNumber
|
|
163
|
+
});
|
|
164
|
+
let pagination = result.pagination || {};
|
|
165
|
+
let users = (result.users?.user || []).map((u: any) => ({
|
|
166
|
+
userId: u.id,
|
|
167
|
+
name: u.name,
|
|
168
|
+
siteRole: u.siteRole
|
|
169
|
+
}));
|
|
170
|
+
return {
|
|
171
|
+
output: { users, totalCount: Number(pagination.totalAvailable || 0) },
|
|
172
|
+
message: `Found **${users.length}** users in group (${pagination.totalAvailable || 0} total).`
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
throw tableauServiceError(`Unknown action: ${action}`);
|
|
177
|
+
})
|
|
178
|
+
.build();
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { createClient } from '../lib/helpers';
|
|
5
|
+
import { tableauServiceError } from '../lib/errors';
|
|
6
|
+
|
|
7
|
+
export let manageJobs = SlateTool.create(spec, {
|
|
8
|
+
name: 'Manage Jobs',
|
|
9
|
+
key: 'manage_jobs',
|
|
10
|
+
description: `List, get details, or cancel background jobs (extract refreshes, flow runs, subscriptions). Use the **action** field to select the operation.`
|
|
11
|
+
})
|
|
12
|
+
.input(
|
|
13
|
+
z.object({
|
|
14
|
+
action: z.enum(['list', 'get', 'cancel']).describe('Operation to perform'),
|
|
15
|
+
jobId: z.string().optional().describe('Job LUID (required for get, cancel)'),
|
|
16
|
+
pageSize: z.number().optional().describe('Page size for list'),
|
|
17
|
+
pageNumber: z.number().optional().describe('Page number for list'),
|
|
18
|
+
filter: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('Filter expression for list (e.g., "jobType:eq:refresh_extracts")')
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
.output(
|
|
25
|
+
z.object({
|
|
26
|
+
jobs: z
|
|
27
|
+
.array(
|
|
28
|
+
z.object({
|
|
29
|
+
jobId: z.string(),
|
|
30
|
+
jobType: z.string().optional(),
|
|
31
|
+
status: z.string().optional(),
|
|
32
|
+
createdAt: z.string().optional(),
|
|
33
|
+
startedAt: z.string().optional(),
|
|
34
|
+
endedAt: z.string().optional(),
|
|
35
|
+
progress: z.number().optional(),
|
|
36
|
+
title: z.string().optional()
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
.optional(),
|
|
40
|
+
job: z
|
|
41
|
+
.object({
|
|
42
|
+
jobId: z.string(),
|
|
43
|
+
jobType: z.string().optional(),
|
|
44
|
+
status: z.string().optional(),
|
|
45
|
+
createdAt: z.string().optional(),
|
|
46
|
+
startedAt: z.string().optional(),
|
|
47
|
+
endedAt: z.string().optional(),
|
|
48
|
+
progress: z.number().optional(),
|
|
49
|
+
title: z.string().optional(),
|
|
50
|
+
finishCode: z.number().optional()
|
|
51
|
+
})
|
|
52
|
+
.optional(),
|
|
53
|
+
totalCount: z.number().optional(),
|
|
54
|
+
cancelled: z.boolean().optional()
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
.handleInvocation(async ctx => {
|
|
58
|
+
let client = createClient(ctx.config, ctx.auth);
|
|
59
|
+
let { action } = ctx.input;
|
|
60
|
+
|
|
61
|
+
if (action === 'list') {
|
|
62
|
+
let result = await client.queryJobs({
|
|
63
|
+
pageSize: ctx.input.pageSize,
|
|
64
|
+
pageNumber: ctx.input.pageNumber,
|
|
65
|
+
filter: ctx.input.filter
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let pagination = result.pagination || {};
|
|
69
|
+
let jobs = (result.backgroundJobs?.backgroundJob || []).map((j: any) => ({
|
|
70
|
+
jobId: j.id,
|
|
71
|
+
jobType: j.jobType,
|
|
72
|
+
status: j.status,
|
|
73
|
+
createdAt: j.createdAt,
|
|
74
|
+
startedAt: j.startedAt,
|
|
75
|
+
endedAt: j.endedAt,
|
|
76
|
+
progress: j.progress != null ? Number(j.progress) : undefined,
|
|
77
|
+
title: j.title
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
output: { jobs, totalCount: Number(pagination.totalAvailable || 0) },
|
|
82
|
+
message: `Found **${jobs.length}** jobs (${pagination.totalAvailable || 0} total).`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (action === 'get') {
|
|
87
|
+
if (!ctx.input.jobId) throw tableauServiceError('jobId is required for get action.');
|
|
88
|
+
|
|
89
|
+
let j = await client.getJob(ctx.input.jobId);
|
|
90
|
+
return {
|
|
91
|
+
output: {
|
|
92
|
+
job: {
|
|
93
|
+
jobId: j.id,
|
|
94
|
+
jobType: j.jobType,
|
|
95
|
+
status: j.status,
|
|
96
|
+
createdAt: j.createdAt,
|
|
97
|
+
startedAt: j.startedAt,
|
|
98
|
+
endedAt: j.endedAt,
|
|
99
|
+
progress: j.progress != null ? Number(j.progress) : undefined,
|
|
100
|
+
title: j.title,
|
|
101
|
+
finishCode: j.finishCode != null ? Number(j.finishCode) : undefined
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
message: `Job \`${j.id}\` is **${j.status}** (type: ${j.jobType}).`
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (action === 'cancel') {
|
|
109
|
+
if (!ctx.input.jobId) throw tableauServiceError('jobId is required for cancel action.');
|
|
110
|
+
|
|
111
|
+
await client.cancelJob(ctx.input.jobId);
|
|
112
|
+
return {
|
|
113
|
+
output: { cancelled: true },
|
|
114
|
+
message: `Cancelled job \`${ctx.input.jobId}\`.`
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw tableauServiceError(`Unknown action: ${action}`);
|
|
119
|
+
})
|
|
120
|
+
.build();
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { createClient } from '../lib/helpers';
|
|
5
|
+
import { tableauServiceError } from '../lib/errors';
|
|
6
|
+
|
|
7
|
+
export let managePermissions = SlateTool.create(spec, {
|
|
8
|
+
name: 'Manage Permissions',
|
|
9
|
+
key: 'manage_permissions',
|
|
10
|
+
description: `Query, add, or delete permissions on Tableau resources (workbooks, datasources, projects, views, flows). Permissions are granted to users or groups with specific capability modes.`,
|
|
11
|
+
instructions: [
|
|
12
|
+
'For resourceType use the plural API path: "workbooks", "datasources", "projects", "views", "flows", or "collections".',
|
|
13
|
+
'For delete, granteeType must be "users" or "groups" (plural).'
|
|
14
|
+
]
|
|
15
|
+
})
|
|
16
|
+
.input(
|
|
17
|
+
z.object({
|
|
18
|
+
action: z.enum(['query', 'add', 'delete']).describe('Operation to perform'),
|
|
19
|
+
resourceType: z
|
|
20
|
+
.enum(['workbooks', 'datasources', 'projects', 'views', 'flows', 'collections'])
|
|
21
|
+
.describe(
|
|
22
|
+
'Resource type path: "workbooks", "datasources", "projects", "views", "flows", or "collections"'
|
|
23
|
+
),
|
|
24
|
+
resourceId: z.string().describe('LUID of the resource'),
|
|
25
|
+
permissions: z
|
|
26
|
+
.array(
|
|
27
|
+
z.object({
|
|
28
|
+
granteeType: z
|
|
29
|
+
.enum(['user', 'group'])
|
|
30
|
+
.describe('Whether the grantee is a user or group'),
|
|
31
|
+
granteeId: z.string().describe('LUID of the user or group'),
|
|
32
|
+
capabilities: z
|
|
33
|
+
.array(
|
|
34
|
+
z.object({
|
|
35
|
+
name: z
|
|
36
|
+
.string()
|
|
37
|
+
.describe('Capability name (e.g., "Read", "Write", "ExportData")'),
|
|
38
|
+
mode: z.enum(['Allow', 'Deny']).describe('Permission mode')
|
|
39
|
+
})
|
|
40
|
+
)
|
|
41
|
+
.describe('Capabilities to grant')
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('Permissions to add (for add action)'),
|
|
46
|
+
granteeType: z
|
|
47
|
+
.enum(['users', 'groups'])
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Grantee type plural (for delete)'),
|
|
50
|
+
granteeId: z.string().optional().describe('LUID of user or group (for delete)'),
|
|
51
|
+
capabilityName: z.string().optional().describe('Capability name to delete (for delete)'),
|
|
52
|
+
capabilityMode: z
|
|
53
|
+
.enum(['Allow', 'Deny'])
|
|
54
|
+
.optional()
|
|
55
|
+
.describe('Capability mode to delete (for delete)')
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
.output(
|
|
59
|
+
z.object({
|
|
60
|
+
permissions: z.any().optional(),
|
|
61
|
+
deleted: z.boolean().optional()
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
.handleInvocation(async ctx => {
|
|
65
|
+
let client = createClient(ctx.config, ctx.auth);
|
|
66
|
+
let { action, resourceType, resourceId } = ctx.input;
|
|
67
|
+
|
|
68
|
+
if (action === 'query') {
|
|
69
|
+
let perms = await client.queryPermissions(resourceType, resourceId);
|
|
70
|
+
return {
|
|
71
|
+
output: { permissions: perms },
|
|
72
|
+
message: `Retrieved permissions for ${resourceType} \`${resourceId}\`.`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (action === 'add') {
|
|
77
|
+
if (!ctx.input.permissions?.length) {
|
|
78
|
+
throw tableauServiceError('permissions is required for add action.');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let perms = await client.addPermissions(resourceType, resourceId, ctx.input.permissions);
|
|
82
|
+
return {
|
|
83
|
+
output: { permissions: perms },
|
|
84
|
+
message: `Added permissions to ${resourceType} \`${resourceId}\`.`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (action === 'delete') {
|
|
89
|
+
if (!ctx.input.granteeType) {
|
|
90
|
+
throw tableauServiceError('granteeType is required for delete action.');
|
|
91
|
+
}
|
|
92
|
+
if (!ctx.input.granteeId) {
|
|
93
|
+
throw tableauServiceError('granteeId is required for delete action.');
|
|
94
|
+
}
|
|
95
|
+
if (!ctx.input.capabilityName) {
|
|
96
|
+
throw tableauServiceError('capabilityName is required for delete action.');
|
|
97
|
+
}
|
|
98
|
+
if (!ctx.input.capabilityMode) {
|
|
99
|
+
throw tableauServiceError('capabilityMode is required for delete action.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await client.deletePermission(
|
|
103
|
+
resourceType,
|
|
104
|
+
resourceId,
|
|
105
|
+
ctx.input.granteeType,
|
|
106
|
+
ctx.input.granteeId,
|
|
107
|
+
ctx.input.capabilityName,
|
|
108
|
+
ctx.input.capabilityMode
|
|
109
|
+
);
|
|
110
|
+
return {
|
|
111
|
+
output: { deleted: true },
|
|
112
|
+
message: `Deleted ${ctx.input.capabilityName} (${ctx.input.capabilityMode}) permission from ${resourceType} \`${resourceId}\`.`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw tableauServiceError(`Unknown action: ${action}`);
|
|
117
|
+
})
|
|
118
|
+
.build();
|