@get-dx/cli 0.2.0 → 0.3.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 +28 -15
- package/dist/cli.js +4 -0
- package/dist/commands/auth.js +27 -22
- package/dist/commands/catalog/entities.js +91 -24
- package/dist/commands/catalog/entitiesRendering.js +13 -9
- package/dist/commands/catalog/entityTypes.js +4 -4
- package/dist/commands/catalog/entityTypesRendering.js +15 -21
- package/dist/commands/init.js +15 -60
- package/dist/commands/scorecards.js +4 -4
- package/dist/commands/scorecardsRendering.js +11 -16
- package/dist/commands/studio/query.js +2 -2
- package/dist/commands/workflows.js +82 -0
- package/dist/commands/workflowsRendering.js +136 -0
- package/dist/config.js +171 -29
- package/dist/http.js +1 -1
- package/dist/loginViaBrowser.js +5 -5
- package/dist/runtime.js +11 -7
- package/dist/ui.js +11 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { password, confirm, select } from "@inquirer/prompts";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { execa } from "execa";
|
|
7
|
-
import { buildRuntime, buildRuntimeSafe } from "../runtime.js";
|
|
7
|
+
import { buildLogger, buildRuntime, buildRuntimeSafe } from "../runtime.js";
|
|
8
8
|
import { getAuthInfo } from "./auth.js";
|
|
9
9
|
import { renderAuthInfo } from "./authRendering.js";
|
|
10
10
|
import { loginViaBrowser } from "../loginViaBrowser.js";
|
|
@@ -13,29 +13,8 @@ import { wrapAction } from "../commandHelpers.js";
|
|
|
13
13
|
import { renderRichText } from "../renderers.js";
|
|
14
14
|
import * as ui from "../ui.js";
|
|
15
15
|
import { CliError } from "../errors.js";
|
|
16
|
-
import {
|
|
16
|
+
import { deriveBaseUrlsFromEnv, persistBaseUrls } from "../config.js";
|
|
17
17
|
import { setToken } from "../secrets.js";
|
|
18
|
-
function parseHostname(raw) {
|
|
19
|
-
const normalized = raw.trim().replace(/\/$/, "");
|
|
20
|
-
if (!normalized || normalized === "app.getdx.com") {
|
|
21
|
-
return { type: "cloud" };
|
|
22
|
-
}
|
|
23
|
-
try {
|
|
24
|
-
const url = new URL(normalized.startsWith("http") ? normalized : `https://${normalized}`);
|
|
25
|
-
const host = url.hostname;
|
|
26
|
-
const dedicatedMatch = host.match(/^(.+)\.getdx\.io$/);
|
|
27
|
-
if (dedicatedMatch) {
|
|
28
|
-
return { type: "dedicated", accountName: dedicatedMatch[1] };
|
|
29
|
-
}
|
|
30
|
-
if (host) {
|
|
31
|
-
return { type: "managed", uiBaseUrl: url.origin };
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
// fall through to invalid
|
|
36
|
-
}
|
|
37
|
-
return { type: "invalid" };
|
|
38
|
-
}
|
|
39
18
|
// FIXME: make this WAY more glam
|
|
40
19
|
// Choose a more interesting ASCII art font from https://patorjk.com/software/taag/
|
|
41
20
|
const WELCOME_BANNER = ui.indent(`\
|
|
@@ -51,14 +30,12 @@ export function initCommand() {
|
|
|
51
30
|
const init = new Command()
|
|
52
31
|
.name("init")
|
|
53
32
|
.description("Initialize the DX CLI")
|
|
54
|
-
.option("--host <hostname>", "DX hostname for dedicated or managed deployments (e.g. mycompany.getdx.io)")
|
|
55
33
|
.action(wrapAction(async (commandOptions, command) => {
|
|
56
34
|
const context = getContext(command);
|
|
57
35
|
let runtime = buildRuntimeSafe(context);
|
|
58
36
|
ensureInteractive();
|
|
59
37
|
showWelcomeBanner();
|
|
60
|
-
|
|
61
|
-
runtime = await ensureLoggedIn(runtime, host);
|
|
38
|
+
runtime = await ensureLoggedIn(context, runtime);
|
|
62
39
|
await optionallySetupSkill(runtime);
|
|
63
40
|
renderRichText([
|
|
64
41
|
ui.blankLine(),
|
|
@@ -75,7 +52,7 @@ function ensureInteractive() {
|
|
|
75
52
|
function showWelcomeBanner() {
|
|
76
53
|
renderRichText([ui.p(ui.success(WELCOME_BANNER))]);
|
|
77
54
|
}
|
|
78
|
-
async function ensureLoggedIn(
|
|
55
|
+
async function ensureLoggedIn(context, runtime) {
|
|
79
56
|
renderRichText([ui.h1(`Checking if you are logged in...`), ui.blankLine()]);
|
|
80
57
|
if (runtime) {
|
|
81
58
|
let authInfo = null;
|
|
@@ -86,34 +63,17 @@ async function ensureLoggedIn(runtime, host) {
|
|
|
86
63
|
// Do nothing, we don't mind if this fails
|
|
87
64
|
}
|
|
88
65
|
if (authInfo) {
|
|
89
|
-
renderAuthInfo(authInfo, runtime.token, runtime.
|
|
66
|
+
renderAuthInfo(authInfo, runtime.token, runtime.apiBaseUrl);
|
|
90
67
|
return runtime;
|
|
91
68
|
}
|
|
92
69
|
}
|
|
93
70
|
renderRichText([ui.p(`You are not logged in yet.`), ui.blankLine()]);
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
throw new CliError(`Could not recognize hostname "${host}". Expected app.getdx.com, <account>.getdx.io, or a custom domain.`);
|
|
97
|
-
}
|
|
98
|
-
switch (parsed.type) {
|
|
99
|
-
case "cloud":
|
|
100
|
-
return await attemptLogin("https://api.getdx.com", "https://app.getdx.com");
|
|
101
|
-
case "dedicated": {
|
|
102
|
-
const { accountName } = parsed;
|
|
103
|
-
return await attemptLogin(`https://api.${accountName}.getdx.io`, `https://${accountName}.getdx.io`);
|
|
104
|
-
}
|
|
105
|
-
case "managed": {
|
|
106
|
-
const apiBaseUrl = await input({
|
|
107
|
-
message: "What is your API base URL?",
|
|
108
|
-
});
|
|
109
|
-
if (!apiBaseUrl) {
|
|
110
|
-
throw new CliError("API base URL is required");
|
|
111
|
-
}
|
|
112
|
-
return await attemptLogin(apiBaseUrl, parsed.uiBaseUrl);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
71
|
+
const { apiBaseUrl, webBaseUrl } = deriveBaseUrlsFromEnv();
|
|
72
|
+
return await attemptLogin(apiBaseUrl, webBaseUrl, context);
|
|
115
73
|
}
|
|
116
|
-
async function attemptLogin(apiBaseUrl,
|
|
74
|
+
async function attemptLogin(apiBaseUrl, webBaseUrl, context) {
|
|
75
|
+
const logger = buildLogger(context);
|
|
76
|
+
logger.debug("Attempting login", { apiBaseUrl, webBaseUrl });
|
|
117
77
|
const method = await select({
|
|
118
78
|
message: "How would you like to log in?",
|
|
119
79
|
choices: [
|
|
@@ -123,7 +83,7 @@ async function attemptLogin(apiBaseUrl, uiBaseUrl) {
|
|
|
123
83
|
});
|
|
124
84
|
let token;
|
|
125
85
|
if (method === "browser") {
|
|
126
|
-
token = await loginViaBrowser(
|
|
86
|
+
token = await loginViaBrowser(webBaseUrl);
|
|
127
87
|
}
|
|
128
88
|
else {
|
|
129
89
|
token = await password({
|
|
@@ -134,17 +94,17 @@ async function attemptLogin(apiBaseUrl, uiBaseUrl) {
|
|
|
134
94
|
if (!token) {
|
|
135
95
|
throw new CliError("Account web API token is required");
|
|
136
96
|
}
|
|
137
|
-
const context = createEmptyContext();
|
|
138
97
|
const runtime = buildRuntime(context, {
|
|
139
|
-
|
|
98
|
+
apiBaseUrl,
|
|
140
99
|
token,
|
|
100
|
+
webBaseUrl,
|
|
141
101
|
});
|
|
142
102
|
renderRichText([ui.blankLine(), ui.p(`Attempting login...`), ui.blankLine()]);
|
|
143
103
|
const response = await getAuthInfo(runtime);
|
|
144
104
|
if (!response.ok) {
|
|
145
105
|
throw new CliError(`Login failed`);
|
|
146
106
|
}
|
|
147
|
-
|
|
107
|
+
persistBaseUrls(apiBaseUrl, webBaseUrl);
|
|
148
108
|
setToken(apiBaseUrl, token);
|
|
149
109
|
renderAuthInfo(response, token, apiBaseUrl);
|
|
150
110
|
return runtime;
|
|
@@ -195,8 +155,3 @@ async function optionallySetupSkill(runtime) {
|
|
|
195
155
|
throw new CliError(`Failed to setup the DX skill: ${result.stderr}`);
|
|
196
156
|
}
|
|
197
157
|
}
|
|
198
|
-
function createEmptyContext() {
|
|
199
|
-
return {
|
|
200
|
-
json: false,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
@@ -53,7 +53,7 @@ export function scorecardsCommand() {
|
|
|
53
53
|
renderJson({ ok: true, scorecard: response.scorecard });
|
|
54
54
|
}
|
|
55
55
|
else {
|
|
56
|
-
renderScorecard(response.scorecard, null, `${ui.success(ui.GLYPHS.CHECK)} Scorecard created`);
|
|
56
|
+
renderScorecard(response.scorecard, null, runtime, `${ui.success(ui.GLYPHS.CHECK)} Scorecard created`);
|
|
57
57
|
}
|
|
58
58
|
}));
|
|
59
59
|
scorecards
|
|
@@ -110,7 +110,7 @@ export function scorecardsCommand() {
|
|
|
110
110
|
renderJson({ ok: true, scorecard: processedScorecard });
|
|
111
111
|
}
|
|
112
112
|
else {
|
|
113
|
-
renderScorecard(response.scorecard, includeSections);
|
|
113
|
+
renderScorecard(response.scorecard, includeSections, runtime);
|
|
114
114
|
}
|
|
115
115
|
}));
|
|
116
116
|
scorecards
|
|
@@ -211,7 +211,7 @@ export function scorecardsCommand() {
|
|
|
211
211
|
renderJson({ ...response, scorecards: processedScorecards });
|
|
212
212
|
}
|
|
213
213
|
else {
|
|
214
|
-
renderScorecardList(response.scorecards, response.response_metadata?.next_cursor ?? null, includeSections);
|
|
214
|
+
renderScorecardList(response.scorecards, response.response_metadata?.next_cursor ?? null, includeSections, runtime);
|
|
215
215
|
}
|
|
216
216
|
}));
|
|
217
217
|
scorecards
|
|
@@ -256,7 +256,7 @@ export function scorecardsCommand() {
|
|
|
256
256
|
renderJson({ ok: true, scorecard: response.scorecard });
|
|
257
257
|
}
|
|
258
258
|
else {
|
|
259
|
-
renderScorecard(response.scorecard, null, `${ui.success(ui.GLYPHS.CHECK)} Scorecard updated`);
|
|
259
|
+
renderScorecard(response.scorecard, null, runtime, `${ui.success(ui.GLYPHS.CHECK)} Scorecard updated`);
|
|
260
260
|
}
|
|
261
261
|
}));
|
|
262
262
|
return scorecards;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { renderRichText } from "../renderers.js";
|
|
2
2
|
import * as ui from "../ui.js";
|
|
3
|
-
export function renderScorecardList(scorecards, nextCursor, includeSections) {
|
|
3
|
+
export function renderScorecardList(scorecards, nextCursor, includeSections, runtime) {
|
|
4
4
|
const shouldIncludeAll = includeSections === null;
|
|
5
5
|
const blocks = [ui.h1("Scorecards")];
|
|
6
6
|
blocks.push(ui.p(`Displaying ${ui.bold(scorecards.length.toString())} scorecards.`));
|
|
@@ -11,7 +11,7 @@ export function renderScorecardList(scorecards, nextCursor, includeSections) {
|
|
|
11
11
|
blocks.push(ui.h2(`${scorecard.name} (${ui.code(scorecard.id)})`));
|
|
12
12
|
if (shouldIncludeAll || includeSections.includes("core")) {
|
|
13
13
|
blocks.push(ui.h3("Basic details"));
|
|
14
|
-
blocks.push(...basicDetailsContent(scorecard));
|
|
14
|
+
blocks.push(...basicDetailsContent(scorecard, runtime));
|
|
15
15
|
blocks.push(ui.h3("Entity filter"));
|
|
16
16
|
blocks.push(...entityFilterContent(scorecard));
|
|
17
17
|
if (scorecard.type === "LEVEL") {
|
|
@@ -34,12 +34,12 @@ export function renderScorecardList(scorecards, nextCursor, includeSections) {
|
|
|
34
34
|
}
|
|
35
35
|
renderRichText(blocks);
|
|
36
36
|
}
|
|
37
|
-
export function renderScorecard(scorecard, includeSections, title = "Scorecard Information") {
|
|
37
|
+
export function renderScorecard(scorecard, includeSections, runtime, title = "Scorecard Information") {
|
|
38
38
|
const shouldIncludeAll = includeSections === null;
|
|
39
39
|
const blocks = [ui.h1(title)];
|
|
40
40
|
if (shouldIncludeAll || includeSections.includes("core")) {
|
|
41
41
|
blocks.push(ui.h2("Basic details"));
|
|
42
|
-
blocks.push(...basicDetailsContent(scorecard));
|
|
42
|
+
blocks.push(...basicDetailsContent(scorecard, runtime));
|
|
43
43
|
blocks.push(ui.h2("Entity filter"));
|
|
44
44
|
blocks.push(...entityFilterContent(scorecard));
|
|
45
45
|
if (scorecard.type === "LEVEL") {
|
|
@@ -61,7 +61,7 @@ export function renderScorecard(scorecard, includeSections, title = "Scorecard I
|
|
|
61
61
|
}
|
|
62
62
|
renderRichText(blocks);
|
|
63
63
|
}
|
|
64
|
-
function basicDetailsContent(scorecard) {
|
|
64
|
+
function basicDetailsContent(scorecard, runtime) {
|
|
65
65
|
const items = [
|
|
66
66
|
ui.dli("ID", [ui.p(ui.code(scorecard.id), false)]),
|
|
67
67
|
ui.dli("Name", [ui.p(scorecard.name, false)]),
|
|
@@ -74,6 +74,9 @@ function basicDetailsContent(scorecard) {
|
|
|
74
74
|
ui.dli("Published", [
|
|
75
75
|
ui.p(scorecard.published ? ui.success("Yes") : ui.dim("No (draft)"), false),
|
|
76
76
|
]),
|
|
77
|
+
ui.dli("Web link", [
|
|
78
|
+
ui.p(ui.link(ui.webLink(`/scorecards/${scorecard.id}`, runtime)), false),
|
|
79
|
+
]),
|
|
77
80
|
];
|
|
78
81
|
if (scorecard.tags && scorecard.tags.length > 0) {
|
|
79
82
|
items.push(ui.dli("Tags", [
|
|
@@ -97,7 +100,7 @@ function entityFilterContent(scorecard) {
|
|
|
97
100
|
else if (scorecard.entity_filter_type === "sql") {
|
|
98
101
|
blocks.push(ui.p(`Filter: ${ui.code("sql")}`));
|
|
99
102
|
if (scorecard.entity_filter_sql) {
|
|
100
|
-
blocks.push(truncatedSqlBlock(scorecard.entity_filter_sql));
|
|
103
|
+
blocks.push(ui.truncatedSqlBlock(scorecard.entity_filter_sql));
|
|
101
104
|
}
|
|
102
105
|
else {
|
|
103
106
|
blocks.push(ui.p(ui.dim("(No SQL provided)")));
|
|
@@ -190,7 +193,7 @@ function checkContent(check, scorecardType) {
|
|
|
190
193
|
}
|
|
191
194
|
// Query
|
|
192
195
|
blocks.push(ui.p(ui.bold("Query:")));
|
|
193
|
-
blocks.push(truncatedSqlBlock(check.sql));
|
|
196
|
+
blocks.push(ui.truncatedSqlBlock(check.sql));
|
|
194
197
|
// Output settings
|
|
195
198
|
if (check.output_enabled) {
|
|
196
199
|
const outputParts = [`Output: ${ui.code(check.output_type ?? "unknown")}`];
|
|
@@ -202,18 +205,10 @@ function checkContent(check, scorecardType) {
|
|
|
202
205
|
// Entity filter
|
|
203
206
|
if (check.filter_sql) {
|
|
204
207
|
blocks.push(ui.p(ui.bold("Entity filter:")));
|
|
205
|
-
blocks.push(truncatedSqlBlock(check.filter_sql));
|
|
208
|
+
blocks.push(ui.truncatedSqlBlock(check.filter_sql));
|
|
206
209
|
if (check.filter_message) {
|
|
207
210
|
blocks.push(ui.p(`Excluded message: ${ui.dim(check.filter_message)}`, false));
|
|
208
211
|
}
|
|
209
212
|
}
|
|
210
213
|
return blocks;
|
|
211
214
|
}
|
|
212
|
-
function truncatedSqlBlock(sql) {
|
|
213
|
-
const lines = sql.split("\n");
|
|
214
|
-
const firstLine = lines[0];
|
|
215
|
-
const isLong = firstLine.length > 80 || lines.length > 1;
|
|
216
|
-
const truncated = firstLine.length > 80 ? firstLine.slice(0, 80) : firstLine;
|
|
217
|
-
const display = isLong ? truncated + ui.dim(" …") : truncated;
|
|
218
|
-
return ui.p(ui.code(display));
|
|
219
|
-
}
|
|
@@ -144,13 +144,13 @@ async function getStudioQueryResults(runtime, id) {
|
|
|
144
144
|
return response.body;
|
|
145
145
|
}
|
|
146
146
|
async function downloadStudioQueryResultsCsv(runtime, id, filename) {
|
|
147
|
-
const response = await fetchWithApiHeaders(runtime, `${runtime.
|
|
147
|
+
const response = await fetchWithApiHeaders(runtime, `${runtime.apiBaseUrl}/studio.queryRuns.results?id=${encodeURIComponent(id)}&format=csv`, { redirect: "manual" });
|
|
148
148
|
if (response.status >= 300 && response.status < 400) {
|
|
149
149
|
const location = response.headers.get("location");
|
|
150
150
|
if (!location) {
|
|
151
151
|
throw new HttpError("CSV download redirect did not include a Location header.", response.status);
|
|
152
152
|
}
|
|
153
|
-
const downloadResponse = await fetchResponse(new URL(location, runtime.
|
|
153
|
+
const downloadResponse = await fetchResponse(new URL(location, runtime.apiBaseUrl).toString(), {
|
|
154
154
|
method: "GET",
|
|
155
155
|
headers: buildDownloadHeaders(runtime, "text/csv"),
|
|
156
156
|
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createExampleText, getContext, wrapAction, } from "../commandHelpers.js";
|
|
3
|
+
import { CliError, EXIT_CODES } from "../errors.js";
|
|
4
|
+
import { request } from "../http.js";
|
|
5
|
+
import { renderJson } from "../renderers.js";
|
|
6
|
+
import { buildRuntime } from "../runtime.js";
|
|
7
|
+
import { renderWorkflowList } from "./workflowsRendering.js";
|
|
8
|
+
const WORKFLOW_SCOPES = ["GLOBAL", "ENTITY"];
|
|
9
|
+
export function workflowsCommand() {
|
|
10
|
+
const workflows = new Command()
|
|
11
|
+
.name("workflows")
|
|
12
|
+
.description("Manage Self-service workflow definitions");
|
|
13
|
+
workflows
|
|
14
|
+
.command("list")
|
|
15
|
+
.description("List Self-service workflows for the account")
|
|
16
|
+
.option("--scope <scope>", "Filter by workflow scope: GLOBAL or ENTITY (omit for both)")
|
|
17
|
+
.option("--entity-identifier <id>", "Catalog entity identifier; must be used with --scope ENTITY")
|
|
18
|
+
.addHelpText("afterAll", createExampleText([
|
|
19
|
+
{
|
|
20
|
+
label: "List workflows the token can trigger",
|
|
21
|
+
command: "dx workflows list",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: "Entity-scoped workflows for a catalog entity",
|
|
25
|
+
command: "dx workflows list --scope ENTITY --entity-identifier acme-app",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: "Same request as JSON",
|
|
29
|
+
command: "dx --json workflows list --scope ENTITY --entity-identifier acme-app",
|
|
30
|
+
},
|
|
31
|
+
]))
|
|
32
|
+
.action(wrapAction(async (options, command) => {
|
|
33
|
+
const scope = parseOptionalScope(options.scope);
|
|
34
|
+
const entityIdentifier = parseOptionalTrimmed(options.entityIdentifier);
|
|
35
|
+
if (entityIdentifier && scope !== "ENTITY") {
|
|
36
|
+
throw new CliError("--entity-identifier requires --scope ENTITY", EXIT_CODES.ARGUMENT_ERROR);
|
|
37
|
+
}
|
|
38
|
+
const runtime = buildRuntime(getContext(command));
|
|
39
|
+
const response = await listWorkflows(runtime, {
|
|
40
|
+
scope,
|
|
41
|
+
entity_identifier: entityIdentifier,
|
|
42
|
+
});
|
|
43
|
+
if (runtime.context.json) {
|
|
44
|
+
renderJson(response);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
renderWorkflowList(response.workflows);
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
return workflows;
|
|
51
|
+
}
|
|
52
|
+
function parseOptionalTrimmed(value) {
|
|
53
|
+
if (typeof value !== "string") {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const t = value.trim();
|
|
57
|
+
return t.length > 0 ? t : undefined;
|
|
58
|
+
}
|
|
59
|
+
function parseOptionalScope(value) {
|
|
60
|
+
const raw = parseOptionalTrimmed(value);
|
|
61
|
+
if (raw === undefined) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
if (!WORKFLOW_SCOPES.includes(raw)) {
|
|
65
|
+
throw new CliError(`--scope must be one of: ${WORKFLOW_SCOPES.join(", ")}`, EXIT_CODES.ARGUMENT_ERROR);
|
|
66
|
+
}
|
|
67
|
+
return raw;
|
|
68
|
+
}
|
|
69
|
+
// --- API ---
|
|
70
|
+
async function listWorkflows(runtime, params) {
|
|
71
|
+
const query = {};
|
|
72
|
+
if (params.scope !== undefined)
|
|
73
|
+
query.scope = params.scope;
|
|
74
|
+
if (params.entity_identifier !== undefined) {
|
|
75
|
+
query.entity_identifier = params.entity_identifier;
|
|
76
|
+
}
|
|
77
|
+
const response = await request(runtime, "/workflows.list", {
|
|
78
|
+
method: "GET",
|
|
79
|
+
query,
|
|
80
|
+
});
|
|
81
|
+
return response.body;
|
|
82
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { renderRichText } from "../renderers.js";
|
|
2
|
+
import * as ui from "../ui.js";
|
|
3
|
+
const WORKFLOW_SCOPE_LABELS = {
|
|
4
|
+
GLOBAL: "Global",
|
|
5
|
+
ENTITY: "Catalog entity",
|
|
6
|
+
};
|
|
7
|
+
const WORKFLOW_EXECUTION_TYPE_LABELS = {
|
|
8
|
+
SIMPLE: "Simple",
|
|
9
|
+
EVENT_DRIVEN: "Event-driven",
|
|
10
|
+
};
|
|
11
|
+
const WORKFLOW_TRIGGER_TYPE_LABELS = {
|
|
12
|
+
ANY: "All users",
|
|
13
|
+
APPROVAL: "All users with approval",
|
|
14
|
+
RUNNERS: "Selected teams and users",
|
|
15
|
+
ENTITY: "Entity owners only",
|
|
16
|
+
};
|
|
17
|
+
const WORKFLOW_TRIGGER_TYPE_DESCRIPTIONS = {
|
|
18
|
+
ANY: "All users can trigger this workflow run without approval.",
|
|
19
|
+
APPROVAL: "All users can request a workflow run, but a designated approver must accept before it runs.",
|
|
20
|
+
RUNNERS: "Only selected teams or users can trigger a workflow run.",
|
|
21
|
+
ENTITY: "Only users who directly own the entity, or belong to an owning team, can trigger a run.",
|
|
22
|
+
};
|
|
23
|
+
function workflowScopeLabel(scope) {
|
|
24
|
+
return WORKFLOW_SCOPE_LABELS[scope] ?? scope;
|
|
25
|
+
}
|
|
26
|
+
function workflowExecutionTypeLabel(executionType) {
|
|
27
|
+
return WORKFLOW_EXECUTION_TYPE_LABELS[executionType] ?? executionType;
|
|
28
|
+
}
|
|
29
|
+
function workflowTriggerTypeLabel(triggerType) {
|
|
30
|
+
return WORKFLOW_TRIGGER_TYPE_LABELS[triggerType] ?? triggerType;
|
|
31
|
+
}
|
|
32
|
+
function workflowTriggerTypeDescription(triggerType) {
|
|
33
|
+
return WORKFLOW_TRIGGER_TYPE_DESCRIPTIONS[triggerType] ?? "";
|
|
34
|
+
}
|
|
35
|
+
export function renderWorkflowList(workflows) {
|
|
36
|
+
const blocks = [
|
|
37
|
+
ui.h1("Workflows"),
|
|
38
|
+
ui.p(`Displaying ${ui.bold(workflows.length.toString())} workflows.`),
|
|
39
|
+
];
|
|
40
|
+
for (const workflow of workflows) {
|
|
41
|
+
blocks.push(ui.h2(`${workflow.name} (${ui.code(workflow.identifier)})`));
|
|
42
|
+
blocks.push(ui.h3("Core attributes"));
|
|
43
|
+
blocks.push(...coreContent(workflow));
|
|
44
|
+
if (workflow.scope === "ENTITY") {
|
|
45
|
+
blocks.push(ui.h3("Entity filter"));
|
|
46
|
+
blocks.push(...entityFilterContent(workflow));
|
|
47
|
+
}
|
|
48
|
+
blocks.push(ui.h3("Parameters"));
|
|
49
|
+
blocks.push(...parametersContent(workflow));
|
|
50
|
+
}
|
|
51
|
+
renderRichText(blocks);
|
|
52
|
+
}
|
|
53
|
+
function coreContent(workflow) {
|
|
54
|
+
const blocks = [];
|
|
55
|
+
const description = workflow.description?.trim()
|
|
56
|
+
? workflow.description
|
|
57
|
+
: ui.dim("(None)");
|
|
58
|
+
blocks.push(ui.dl([
|
|
59
|
+
ui.dli("Description", description),
|
|
60
|
+
ui.dli("Scope", workflowScopeLabel(workflow.scope)),
|
|
61
|
+
ui.dli("Execution type", workflowExecutionTypeLabel(workflow.execution_type)),
|
|
62
|
+
ui.dli("Who can run", `${workflowTriggerTypeLabel(workflow.trigger_type)} ${ui.dim(workflowTriggerTypeDescription(workflow.trigger_type))}`),
|
|
63
|
+
], { termWidth: 16 }));
|
|
64
|
+
return blocks;
|
|
65
|
+
}
|
|
66
|
+
function entityFilterContent(workflow) {
|
|
67
|
+
const blocks = [];
|
|
68
|
+
if (workflow.entity_filter_type === "ENTITY_TYPES") {
|
|
69
|
+
blocks.push(ui.p(`Filter type: ${ui.code("Entity types")}`));
|
|
70
|
+
if (workflow.entity_filter_type_identifiers &&
|
|
71
|
+
workflow.entity_filter_type_identifiers.length > 0) {
|
|
72
|
+
blocks.push(ui.ul(workflow.entity_filter_type_identifiers.map((identifier) => ui.li([ui.p(ui.code(identifier), false)]))));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (workflow.entity_filter_type === "SQL") {
|
|
76
|
+
blocks.push(ui.p(`Filter type: ${ui.code("SQL")}`));
|
|
77
|
+
if (workflow.entity_filter_sql) {
|
|
78
|
+
blocks.push(ui.truncatedSqlBlock(workflow.entity_filter_sql));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return blocks;
|
|
82
|
+
}
|
|
83
|
+
function parametersContent(workflow) {
|
|
84
|
+
if (workflow.parameters.length === 0) {
|
|
85
|
+
return [ui.p(ui.dim("(No parameters)"))];
|
|
86
|
+
}
|
|
87
|
+
return [
|
|
88
|
+
ui.ul(workflow.parameters.map((parameter) => ui.li(singleParameterContent(parameter)))),
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
function singleParameterContent(parameter) {
|
|
92
|
+
const requiredPart = parameter.is_required ? ` ${ui.dim("required")}` : "";
|
|
93
|
+
const headline = `${ui.bold(parameter.name)} (${ui.code(parameter.identifier)}) ${ui.code(parameter.type)}${requiredPart}`;
|
|
94
|
+
const blocks = [ui.p(headline, false)];
|
|
95
|
+
const desc = parameter.description?.trim();
|
|
96
|
+
if (desc) {
|
|
97
|
+
blocks.push(ui.p(desc, false));
|
|
98
|
+
}
|
|
99
|
+
if (parameter.default_value !== null &&
|
|
100
|
+
parameter.default_value !== undefined &&
|
|
101
|
+
parameter.default_value !== "") {
|
|
102
|
+
blocks.push(ui.p(`Default: ${ui.code(formatParameterDefault(parameter.default_value))}`, false));
|
|
103
|
+
}
|
|
104
|
+
if (parameter.type === "SELECT") {
|
|
105
|
+
const options = selectParameterOptions(parameter.definition);
|
|
106
|
+
if (options.length > 0) {
|
|
107
|
+
blocks.push(ui.p("Options:", false));
|
|
108
|
+
blocks.push(ui.ul(options.map((value) => ui.li([ui.p(ui.code(value), false)]))));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return blocks;
|
|
112
|
+
}
|
|
113
|
+
function selectParameterOptions(definition) {
|
|
114
|
+
if (!definition || typeof definition !== "object") {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const raw = definition.options;
|
|
118
|
+
if (!Array.isArray(raw)) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
return raw.filter((item) => typeof item === "string");
|
|
122
|
+
}
|
|
123
|
+
function formatParameterDefault(value) {
|
|
124
|
+
if (typeof value === "string") {
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
128
|
+
return String(value);
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
return JSON.stringify(value);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return String(value);
|
|
135
|
+
}
|
|
136
|
+
}
|