Skip to content

Commit

Permalink
feat: add ui mode
Browse files Browse the repository at this point in the history
add ui mode.
fix incorrect table output length.
refactor quite to silent.
add temporary patch to prompts module.
  • Loading branch information
hoonoh committed Oct 18, 2019
1 parent 51ff4c9 commit 0bf8a0f
Show file tree
Hide file tree
Showing 10 changed files with 699 additions and 52 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ CLI utility to list current global AWS EC2 Spot Instance prices. Requires valid

## Options

### --ui

Start with UI mode.

### --region | -r

AWS region to fetch data from. Accepts multiple string values.
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"license": "MIT",
"scripts": {
"prepublishOnly": "yarn clean && yarn build",
"postinstall": "patch-package",
"clean": "rm -rf dist && rm -rf coverage",
"build:ec2-types": "ts-node -T util/generate-ec2-types.ts",
"build": "tsc",
Expand Down Expand Up @@ -68,6 +69,8 @@
"dependencies": {
"aws-sdk": "^2.544.0",
"lodash": "^4.17.15",
"patch-package": "^6.2.0",
"prompts": "^2.2.1",
"table": "^5.4.6",
"yargs": "^14.2.0"
},
Expand All @@ -77,6 +80,7 @@
"@types/lodash": "^4.14.144",
"@types/node": "^12.7.5",
"@types/prettier": "^1.18.3",
"@types/prompts": "^2.0.1",
"@types/table": "^4.0.7",
"@types/yargs": "^13.0.3",
"@typescript-eslint/eslint-plugin": "^2.3.0",
Expand All @@ -95,6 +99,7 @@
"jest": "^24.9.0",
"jest-mock-console": "^1.0.0",
"nock": "^11.4.0",
"postinstall-postinstall": "^2.0.0",
"prettier": "^1.18.2",
"ts-jest": "^24.1.0",
"ts-node": "^8.4.1",
Expand Down
26 changes: 26 additions & 0 deletions patches/prompts+2.2.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
diff --git a/node_modules/prompts/lib/elements/autocompleteMultiselect.js b/node_modules/prompts/lib/elements/autocompleteMultiselect.js
index 16ec459..5901c3c 100644
--- a/node_modules/prompts/lib/elements/autocompleteMultiselect.js
+++ b/node_modules/prompts/lib/elements/autocompleteMultiselect.js
@@ -123,13 +123,19 @@ class AutocompleteMultiselectPrompt extends MultiselectPrompt {
}

renderInstructions() {
- return `
+ if (this.instructions === undefined || this.instructions) {
+ if (typeof this.instructions === 'string') {
+ return this.instructions;
+ }
+ return `
Instructions:
${figures.arrowUp}/${figures.arrowDown}: Highlight option
${figures.arrowLeft}/${figures.arrowRight}/[space]: Toggle selection
[a,b,c]/delete: Filter choices
enter/return: Complete answer
- `
+`;
+ }
+ return '';
}

renderCurrentInput() {
171 changes: 171 additions & 0 deletions src/cli-ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { Choice, prompt } from 'prompts';

import {
allInstances,
instanceFamily,
InstanceFamilyType,
instanceFamilyTypes,
InstanceSize,
instanceSizes,
} from './ec2-types';
import { allProductDescriptions, ProductDescription } from './product-description';
import { allRegions, Region, regionNames } from './regions';

type Answer1 = { region: Region[]; family: (keyof typeof instanceFamily)[] };

type Answer2 = {
familyType: InstanceFamilyType[];
size: InstanceSize[];
productDescription: ProductDescription[];
maxPrice: number;
limit: number;
accessKeyId: string;
secretAccessKey: string;
};

export type Answers = Answer1 & Answer2;

export const ui = async (): Promise<Answers | undefined> => {
try {
const question1 = [
{
type: 'autocompleteMultiselect',
name: 'region',
message: 'Select regions (select none for default regions)',
instructions: false,
choices: allRegions.reduce(
(list, region) => {
list.push({
title: `${regionNames[region]} - ${region}`,
value: region,
});
return list;
},
[] as Choice[],
),
},
{
type: 'multiselect',
name: 'family',
message: 'Select EC2 Family',
instructions: false,
choices: Object.keys(instanceFamily).reduce(
(list, family) => {
list.push({
title: family,
value: family,
});
return list;
},
[] as Choice[],
),
},
];

const answer1: Answer1 = await prompt(question1);

const familyTypePreSelectedSet = new Set<InstanceFamilyType>();
const sizePreSelectedSet = new Set<InstanceSize>();
if (answer1.family.length > 0) {
answer1.family.forEach(family => {
instanceFamily[family].forEach((type: InstanceFamilyType) => {
familyTypePreSelectedSet.add(type);
allInstances
.filter(instance => instance.startsWith(type))
.forEach(instance => {
sizePreSelectedSet.add(instance.split('.').pop() as InstanceSize);
});
});
});
}
const familyTypePreSelected = Array.from(familyTypePreSelectedSet);
const sizePreSelected = Array.from(sizePreSelectedSet);

let familyTypePreSelectMessage = '(select none to include all)';
if (answer1.family.length > 0) {
const last = answer1.family.pop();
const list = answer1.family.length ? `${answer1.family.join(', ')} and ${last}` : last;
familyTypePreSelectMessage = `(${list} sizes are pre-selected)`;
}

const question2 = [
{
type: 'autocompleteMultiselect',
name: 'familyType',
message: `Select EC2 Family Type ${familyTypePreSelectMessage}`,
instructions: false,
choices: instanceFamilyTypes.reduce(
(list, familyType) => {
list.push({
title: familyType,
value: familyType,
selected: familyTypePreSelected.includes(familyType),
});
return list;
},
[] as (Choice | { selected: boolean })[],
),
},
{
type: 'autocompleteMultiselect',
name: 'size',
message: `Select EC2 Family Size ${familyTypePreSelectMessage}`,
instructions: false,
choices: instanceSizes.reduce(
(list, size) => {
list.push({
title: size,
value: size,
selected: sizePreSelected.includes(size),
});
return list;
},
[] as (Choice | { selected: boolean })[],
),
},
{
type: 'autocompleteMultiselect',
name: 'productDescription',
message: `Select EC2 Product description (select none to include all)`,
instructions: false,
choices: allProductDescriptions.map(desc => ({
title: desc,
value: desc,
})),
},
{
type: 'number',
name: 'maxPrice',
message: `Select maximum price`,
initial: 5,
float: true,
round: 4,
increment: 0.0001,
min: 0.0015,
},
{
type: 'number',
name: 'limit',
message: `Select result limit`,
initial: 20,
min: 1,
},
{
type: 'text',
name: 'accessKeyId',
message: `Enter AWS accessKeyId (optional)`,
},
{
type: 'invisible',
name: 'secretAccessKey',
message: `Enter AWS secretAccessKey (optional)`,
},
];

const answer2: Answer2 = await prompt(question2);

return { ...answer1, ...answer2 };
} catch (error) {
return undefined;
}
};
99 changes: 79 additions & 20 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sep } from 'path';
import * as yargs from 'yargs';

import { Answers, ui } from './cli-ui';
import {
allInstances,
instanceFamily,
Expand Down Expand Up @@ -100,6 +101,11 @@ export const main = (argvInput?: string[]): Promise<void> =>
describe: 'AWS Secret Access Key.',
type: 'string',
},
ui: {
describe: 'Start with UI mode',
type: 'boolean',
default: false,
},
},

async args => {
Expand Down Expand Up @@ -197,19 +203,6 @@ export const main = (argvInput?: string[]): Promise<void> =>
const familyTypeSetArray = Array.from(familyTypeSet);
const sizeSetArray = Array.from(sizeSet);

console.log('Querying current spot prices with options:');
console.group();
console.log('limit:', limit);
if (region) console.log('regions:', region.join(', '));
if (instanceType) console.log('instanceTypes:', instanceType.join(', '));
if (familyTypeSetArray.length)
console.log('familyTypes:', familyTypeSetArray.join(', '));
if (sizeSetArray.length) console.log('sizes:', sizeSetArray.join(', '));
if (priceMax) console.log('priceMax:', priceMax);
if (productDescriptionsSetArray.length)
console.log('productDescriptions:', productDescriptionsSetArray.join(', '));
console.groupEnd();

await getGlobalSpotPrices({
regions: region as Region[],
instanceTypes: instanceType as InstanceType[],
Expand Down Expand Up @@ -242,18 +235,84 @@ export const main = (argvInput?: string[]): Promise<void> =>
y.exitProcess(false);
y.parse(argvInput);
if (argvInput.includes('--help')) res();
} else if (process.argv.includes('--ui')) {
ui().then(answers => {
if (answers) {
let params: string[] = [];
(Object.keys(answers) as (keyof Answers)[]).forEach(p => {
switch (p) {
case 'region':
if (answers[p].length) {
params.push('-r');
params = [...params, ...answers[p]];
}
break;

case 'familyType':
if (answers[p].length) {
params.push('-f');
params = [...params, ...answers[p]];
}
break;
case 'size':
if (answers[p].length) {
params.push('-s');
params = [...params, ...answers[p]];
}
break;
case 'productDescription':
if (answers[p].length) {
params.push('-d');
params = [...params, ...answers[p]];
}
break;
case 'maxPrice':
if (answers[p]) {
params.push('-p');
params.push(answers[p].toString());
}
break;
case 'limit':
if (answers[p]) {
params.push('-l');
params.push(answers[p].toString());
}
break;

case 'accessKeyId':
if (answers[p]) {
params.push('--accessKeyId');
params.push(answers[p]);
}
break;
case 'secretAccessKey':
if (answers[p]) {
params.push('--secretAccessKey');
params.push(answers[p]);
}
break;

default:
break;
}
});
y.parse(params);
} else {
console.log('aborted');
}
});
} else {
y.parse(process.argv);
}

/* istanbul ignore next */
const cleanExit = (): void => {
process.exit();
};
process.on('SIGINT', cleanExit); // catch ctrl-c
process.on('SIGTERM', cleanExit); // catch kill
});

/* istanbul ignore next */
const cleanExit = (): void => {
process.exit();
};
process.on('SIGINT', cleanExit); // catch ctrl-c
process.on('SIGTERM', cleanExit); // catch kill

/* istanbul ignore if */
if (
require.main &&
Expand Down
Loading

0 comments on commit 0bf8a0f

Please sign in to comment.