Skip to content
This repository was archived by the owner on Feb 13, 2024. It is now read-only.

Commit afc7c53

Browse files
authored
Prompt for parameters during installation (#34)
1 parent 8e7cae0 commit afc7c53

File tree

8 files changed

+200
-10
lines changed

8 files changed

+200
-10
lines changed

package-lock.json

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,10 @@
223223
"dependencies": {
224224
"@types/lodash": "^4.14.113",
225225
"@types/shelljs": "^0.8.0",
226+
"@types/tmp": "0.0.33",
226227
"lodash": "^4.17.10",
227-
"shelljs": "^0.7.7"
228+
"shelljs": "^0.7.7",
229+
"tmp": "^0.0.33"
228230
},
229231
"devDependencies": {
230232
"typescript": "^2.6.1",
@@ -237,4 +239,4 @@
237239
"type": "git",
238240
"url": "https://github.com/deis/duffle-vscode"
239241
}
240-
}
242+
}

src/commands/install.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { succeeded, map, Errorable } from '../utils/errorable';
88
import * as shell from '../utils/shell';
99
import { cantHappen } from '../utils/never';
1010
import { promptBundle, BundleSelection, fileBundleSelection, repoBundleSelection } from '../utils/bundleselection';
11+
import { promptForParameters } from '../utils/parameters';
12+
import { withOptionalTempFile } from '../utils/tempfile';
1113

1214
export async function install(target?: any): Promise<void> {
1315
if (!target) {
@@ -50,7 +52,12 @@ async function installCore(bundlePick: BundleSelection): Promise<void> {
5052
return;
5153
}
5254

53-
const installResult = await installTo(bundlePick, name);
55+
const parameterValues = await promptForParameters(bundlePick, 'Install', 'Enter installation parameters');
56+
if (parameterValues.cancelled) {
57+
return;
58+
}
59+
60+
const installResult = await installToViaTempFile(bundlePick, name, parameterValues.values);
5461

5562
if (succeeded(installResult)) {
5663
await refreshBundleExplorer();
@@ -59,17 +66,22 @@ async function installCore(bundlePick: BundleSelection): Promise<void> {
5966
await showDuffleResult('install', (bundleId) => bundleId, installResult);
6067
}
6168

62-
async function installTo(bundlePick: BundleSelection, name: string): Promise<Errorable<string>> {
69+
async function installToViaTempFile(bundlePick: BundleSelection, name: string, parameterValues: any): Promise<Errorable<string>> {
70+
const parametersJSON = parameterValues ? JSON.stringify(parameterValues, undefined, 2) : undefined;
71+
return withOptionalTempFile(parametersJSON, 'json', (paramsFile) => installTo(bundlePick, name, paramsFile));
72+
}
73+
74+
async function installTo(bundlePick: BundleSelection, name: string, paramsFile: string | undefined): Promise<Errorable<string>> {
6375
if (bundlePick.kind === 'folder') {
6476
const folderPath = bundlePick.path;
6577
const bundlePath = path.join(folderPath, "cnab", "bundle.json");
6678
const installResult = await longRunning(`Duffle installing ${bundlePath}`,
67-
() => duffle.installFile(shell.shell, bundlePath, name)
79+
() => duffle.installFile(shell.shell, bundlePath, name, paramsFile)
6880
);
6981
return map(installResult, (_) => bundlePath);
7082
} else if (bundlePick.kind === 'repo') {
7183
const installResult = await longRunning(`Duffle installing ${bundlePick.bundle}`,
72-
() => duffle.installBundle(shell.shell, bundlePick.bundle, name)
84+
() => duffle.installBundle(shell.shell, bundlePick.label /* because bundlePick.bundle doesn't work */, name, paramsFile)
7385
);
7486
return map(installResult, (_) => bundlePick.bundle);
7587
}

src/duffle/duffle.objectmodel.ts

+8
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,11 @@ export interface RepoBundle {
1111
readonly repository: string;
1212
readonly version: string;
1313
}
14+
15+
export interface ParameterDefinition {
16+
readonly name: string;
17+
readonly type: string;
18+
readonly allowedValues?: (number | string | boolean)[];
19+
readonly defaultValue?: number | string | boolean;
20+
readonly metadata?: { description?: string };
21+
}

src/duffle/duffle.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,16 @@ export async function build(sh: shell.Shell, folderPath: string): Promise<Errora
8888
return await invokeObj(sh, 'build', '.', { cwd: folderPath }, (s) => null);
8989
}
9090

91-
export async function installFile(sh: shell.Shell, bundleFilePath: string, name: string): Promise<Errorable<null>> {
92-
return await invokeObj(sh, 'install', `${name} -f "${bundleFilePath}"`, {}, (s) => null);
91+
export async function installFile(sh: shell.Shell, bundleFilePath: string, name: string, paramsFile: string | undefined): Promise<Errorable<null>> {
92+
return await invokeObj(sh, 'install', `${name} -f "${bundleFilePath}" ${paramsArg(paramsFile)}`, {}, (s) => null);
9393
}
9494

95-
export async function installBundle(sh: shell.Shell, bundleName: string, name: string): Promise<Errorable<null>> {
96-
return await invokeObj(sh, 'install', `${name} ${bundleName}`, {}, (s) => null);
95+
export async function installBundle(sh: shell.Shell, bundleName: string, name: string, paramsFile: string | undefined): Promise<Errorable<null>> {
96+
return await invokeObj(sh, 'install', `${name} ${bundleName} ${paramsArg(paramsFile)}`, {}, (s) => null);
97+
}
98+
99+
function paramsArg(file: string | undefined): string {
100+
return file ? `-p "${file}"` : '';
97101
}
98102

99103
function fromHeaderedTable<T>(lines: string[]): T[] {

src/utils/dialog.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as vscode from 'vscode';
2+
3+
export const END_DIALOG_FN = "endDialog()";
4+
5+
export function dialog(tabTitle: string, htmlBody: string, formId: string): Promise<any> {
6+
return new Promise<any>((resolve, reject) => {
7+
const postbackScript = `<script>
8+
function ${END_DIALOG_FN} {
9+
const vscode = acquireVsCodeApi();
10+
const s = {};
11+
for (const e of document.forms['${formId}'].elements) {
12+
s[e.name] = e.value;
13+
}
14+
vscode.postMessage(s);
15+
}
16+
</script>`;
17+
18+
const html = postbackScript + htmlBody;
19+
const w = vscode.window.createWebviewPanel('duffle-dialog', tabTitle, vscode.ViewColumn.Active, {
20+
retainContextWhenHidden: false,
21+
enableScripts: true,
22+
});
23+
w.webview.html = html;
24+
const cancelSubscription = w.onDidDispose(() => resolve(undefined));
25+
w.webview.onDidReceiveMessage((m) => {
26+
cancelSubscription.dispose();
27+
w.dispose();
28+
resolve(m);
29+
});
30+
w.reveal();
31+
});
32+
}

src/utils/parameters.ts

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as path from 'path';
2+
3+
import { fs } from './fs';
4+
import { ParameterDefinition } from "../duffle/duffle.objectmodel";
5+
import { BundleSelection } from "./bundleselection";
6+
import { END_DIALOG_FN, dialog } from "./dialog";
7+
import { cantHappen } from './never';
8+
9+
interface ParameterValues {
10+
readonly cancelled: false;
11+
readonly values: any;
12+
}
13+
14+
interface Cancelled {
15+
readonly cancelled: true;
16+
}
17+
18+
export type ParameterValuesPromptResult = ParameterValues | Cancelled;
19+
20+
export async function promptForParameters(bundlePick: BundleSelection, actionName: string, prompt: string): Promise<ParameterValuesPromptResult> {
21+
const definitions = await bundleParameters(bundlePick);
22+
if (!definitions || definitions.length === 0) {
23+
return { cancelled: false, values: {} };
24+
}
25+
26+
const parameterFormId = 'pvform';
27+
28+
const html = `<h1>${prompt}</h1>
29+
<form id='${parameterFormId}'>
30+
${parameterEntryTable(definitions)}
31+
</form>
32+
<p><button onclick='${END_DIALOG_FN}'>${actionName}</button></p>`;
33+
34+
const parameterValues = await dialog(`${actionName} ${bundlePick.label}`, html, parameterFormId);
35+
if (!parameterValues) {
36+
return { cancelled: true };
37+
}
38+
39+
return { cancelled: false, values: parameterValues };
40+
}
41+
42+
function parameterEntryTable(ps: ParameterDefinition[]): string {
43+
const rows = ps.map(parameterEntryRow).join('');
44+
return `<table>${rows}</table>`;
45+
}
46+
47+
function parameterEntryRow(p: ParameterDefinition): string {
48+
return `<tr valign="baseline">
49+
<td><b>${p.name}</b></td>
50+
<td>${inputWidget(p)}</td>
51+
</tr>
52+
<tr>
53+
<td colspan="2" style="font-size:80%">${p.metadata ? p.metadata.description : ''}</td>
54+
</tr>
55+
`;
56+
}
57+
58+
function inputWidget(p: ParameterDefinition): string {
59+
if (p.type === "boolean") {
60+
return `<select name="${p.name}"><option>True</option><option>False</option></select>`;
61+
}
62+
if (p.allowedValues) {
63+
const opts = p.allowedValues.map((av) => `<option>${av}</option>`).join('');
64+
return `<select name="${p.name}">${opts}</select>`;
65+
}
66+
const defval = p.defaultValue ? `${p.defaultValue}` : '';
67+
return `<input name="${p.name}" type="text" value="${defval}" />`;
68+
}
69+
70+
function localPath(bundleRef: string): string {
71+
const bits = bundleRef.split('/');
72+
const last = bits.pop()!;
73+
bits.push('bundles', last);
74+
return bits.join('/');
75+
}
76+
77+
async function bundleParameters(bundlePick: BundleSelection): Promise<ParameterDefinition[]> {
78+
if (bundlePick.kind === "folder") {
79+
return await parseParametersFromJSONFile(path.join(bundlePick.path, "bundle.json"));
80+
} else if (bundlePick.kind === "repo") {
81+
return await parseParametersFromJSONFile(path.join(process.env["USERPROFILE"]!, ".duffle", "repositories", localPath(bundlePick.bundle) + '.json'));
82+
}
83+
return cantHappen(bundlePick);
84+
}
85+
86+
async function parseParametersFromJSONFile(jsonFile: string): Promise<ParameterDefinition[]> {
87+
const json = await fs.readFile(jsonFile, 'utf8');
88+
const parameters = JSON.parse(json).parameters;
89+
const defs: ParameterDefinition[] = [];
90+
if (parameters) {
91+
for (const k in parameters) {
92+
defs.push({ name: k, ...parameters[k] });
93+
}
94+
}
95+
return defs;
96+
}

src/utils/tempfile.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as tmp from 'tmp';
2+
3+
import { fs } from './fs';
4+
5+
export async function withOptionalTempFile<T>(content: string | undefined, fileType: string, fn: (filename: string | undefined) => Promise<T>): Promise<T> {
6+
if (!content) {
7+
return fn(undefined);
8+
}
9+
10+
const tempFile = tmp.fileSync({ prefix: "vsduffle-", postfix: `.${fileType}` });
11+
await fs.writeFile(tempFile.name, content);
12+
13+
try {
14+
return await fn(tempFile.name);
15+
} finally {
16+
tempFile.removeCallback();
17+
}
18+
}

0 commit comments

Comments
 (0)