@kui-shell/core
Version:
An Electron-based shell for cloud-native development
671 lines (668 loc) • 24.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.UsageError = void 0;
exports.isMessageWithCode = isMessageWithCode;
exports.isMessageWithUsageModel = isMessageWithUsageModel;
exports.isUsageError = isUsageError;
var _debug = _interopRequireDefault(require("debug"));
var _capabilities = require("./capabilities");
var _types = require("../util/types");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/*
* Copyright 2017 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
const debug = (0, _debug.default)('core/usage-error');
debug('loading');
class DefaultUsageOptions {}
/** lazily load repl */
function pexec(command) {
return () => __awaiter(this, void 0, void 0, function* () {
const {
pexec
} = yield Promise.resolve().then(() => require('../repl/exec'));
pexec(command);
});
}
/**
* Map a asynchronous function to an array sequentially from front to
* back.
*
*/
function promiseEach(arr, fn) {
return __awaiter(this, void 0, void 0, function* () {
const result = [];
for (const item of arr) {
result.push(yield fn(item));
}
return result;
});
}
/** Create an HTML DIV to wrap around the given string */
const div = (str, css = undefined, tag = 'div') => {
const result = document.createElement(tag);
if (str) {
if (typeof str === 'string') {
Promise.resolve(str).then(str => {
result.innerText = str;
});
} else {
result.appendChild(str);
}
}
if (css) {
if (typeof css === 'string') {
result.className = css;
} else {
css.forEach(_ => result.classList.add(_));
}
}
return result;
};
const span = (str, css) => div(str, css, 'span');
/**
* The start of every section, e.g. Usage:
*
*/
const prefix = str => {
const prefix = div(str, 'usage-error-title', 'h4');
prefix.setAttribute('data-title', str);
return prefix;
};
/** A part of the main body of the usage message */
const bodyPart = (noMargin = false) => {
const result = div();
if (!noMargin) result.style.margin = '2em 0em 0 0';
return result;
};
/** render the given div with the default san serif font */
const sans = div => {
div.classList.add('sans-serif');
return div;
};
/** render the given div a bit smaller */
const smaller = div => {
div.classList.add('somewhat-smaller-text');
return div;
};
/** render the given div with white space line wrapping */
const wrap = div => {
div.style.display = 'block';
div.style.whiteSpace = 'normal';
return div;
};
function isBreadcrumbWithClickCommand(crumb) {
const breadcrumb = crumb;
return !!(breadcrumb.label && breadcrumb.command);
}
const makeBreadcrumb = options => {
let cmd;
let label;
if (typeof options.breadcrumb === 'string') {
cmd = label = options.breadcrumb;
} else if (isBreadcrumbWithClickCommand(options.breadcrumb)) {
cmd = options.breadcrumb.command;
label = options.breadcrumb.label;
} else {
cmd = options.breadcrumb.command;
label = breadcrumbFromCommand(cmd);
}
return Promise.resolve(label).then(label => {
const item = span();
item.classList.add('bx--breadcrumb-item');
const dom = span(label, 'bx--no-link');
dom.setAttribute('data-label', label);
item.appendChild(dom);
if (!options.noSlash) {
item.appendChild(span((0, _capabilities.isHeadless)() ? '/' : '', 'bx--breadcrumb-item--slash'));
}
if (cmd) {
dom.classList.add('bx--link');
dom.onclick = pexec(cmd);
}
return item;
});
};
function isGenerator(row) {
return !!row.fn;
}
function isMessageWithUsageModel(msg) {
const message = msg;
return !!message.usage;
}
function isMessageWithCode(msg) {
const message = msg;
return !!(message.message && (message.code || message.statusCode || message.exitCode));
}
class UsageError extends Error {
constructor(message, extra) {
super();
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
this.raw = message;
this.code = isMessageWithCode(message) ? message.statusCode || message.code || message.exitCode : 500;
if (typeof message === 'string') {
this.message = message;
} else {
this.formattedMessage = format(message, extra);
}
}
getUsageModel() {
return this.raw.usage;
}
static getFormattedMessage(err) {
if (err.formattedMessage && !err.formattedMessage.then && Object.keys(err.formattedMessage).length === 0) {
err.formattedMessage = format(err.raw);
}
return err.formattedMessage ? err.formattedMessage : Promise.resolve(span(err.message));
}
static isUsageError(error) {
const err = error;
return !!(err.formattedMessage && err.code);
}
}
exports.UsageError = UsageError;
function isUsageError(error) {
return UsageError.isUsageError(error);
}
/**
* Invoke a given command, and extract the breadcrumb title from the resulting usage model
*
*/
const breadcrumbFromCommand = command => __awaiter(void 0, void 0, void 0, function* () {
const {
qexec
} = yield Promise.resolve().then(() => require('../repl/exec'));
const usageError = yield qexec(command, undefined, undefined, {
failWithUsage: true
});
if (isMessageWithUsageModel(usageError.raw)) {
const usage = usageError.raw.usage;
return usage.breadcrumb || usage.title;
} else {
return usageError.message;
}
});
/**
* Format the given usage message
*
*/
const format = (message, options = new DefaultUsageOptions()) => __awaiter(void 0, void 0, void 0, function* () {
debug('format message', message);
if (typeof message === 'string') {
// then the content is already formatted
return Promise.resolve(span(message));
} else if ((0, _types.isHTML)(message)) {
// same...
return Promise.resolve(message);
/* } else if (isGenerator<UsageLike>(message)) {
// then message.fn is a generator, e.g. for command aliases
return format(message.fn(message.command), undefined, options) */
} else {
// these are the fields of the usage message
const usage = message.usage || {};
const {
command,
docs,
title,
breadcrumb = title || command,
header = docs && `${docs}.`,
example,
detailedExample,
sampleInputs,
intro,
sections,
// the more general case: the usage model has custom sections
commandPrefix,
commandPrefixNotNeeded,
commandSuffix = '',
preserveCase = false,
// in breadcrumbs
available,
related,
required,
optional,
oneof
} = usage;
const outerCommandPrefix = commandPrefix;
const outerCommandSuffix = commandSuffix;
const outerCommand = command;
// the return value will be `result`; we will populate it with
// those fields now; `body` is the flex-wrap portion of the
// content
const resultWrapper = div(undefined, 'usage-error-wrapper');
const result = div(undefined, options.noHide ? '' : ['hideable', 'page-content'], 'p');
const body = div();
const left = div(); // usage and detailedExample
const right = div(); // required and optional parameters
// if we have a great many detailed examples, place them in a scroll region
const scrollableDetailedExamples = detailedExample && Array.isArray(detailedExample) && detailedExample.length > 4;
if (sections && sections.length > 1 || scrollableDetailedExamples && sections && sections.length > 0) {
left.classList.add('fifty-fifty');
right.classList.add('fifty-fifty');
} else {
left.style.marginRight = '2em';
}
if (message.message) {
// then the repl wrapped around the usage model, adding an extra message string
const messageString = message.message;
const messageDom = div(undefined, '', 'div');
const prefacePart = span('');
const messagePart = span(messageString, 'red-text usage-error-message-string');
if (message.messageDom) {
if (typeof message.messageDom === 'string') {
messagePart.appendChild(document.createTextNode(message.messageDom));
} else {
messagePart.appendChild(message.messageDom);
}
}
if (!options.noHide) {
result.classList.add('hidden');
}
messageDom.appendChild(prefacePart);
messageDom.appendChild(messagePart);
resultWrapper.appendChild(messageDom);
if (!(0, _capabilities.isHeadless)() && !options.noHide) {
const usagePart = div(undefined, 'small-top-pad hideable click-here-for-usage-container');
const frontPart = span('Click ');
const clickyPart = span('here', 'clickable clickable-blatant click-here-for-usage');
const restPart = span(' for usage information.');
usagePart.appendChild(frontPart);
usagePart.appendChild(clickyPart);
usagePart.appendChild(restPart);
messageDom.appendChild(usagePart);
clickyPart.onclick = () => {
result.classList.remove('hidden');
usagePart.classList.add('hidden');
};
}
}
resultWrapper.appendChild(result);
//
// breadcrumb
//
if (!options.noBreadcrumb) {
// breadcrumb model chain
const parentChain = (usage.parents || []).map(breadcrumb => ({
breadcrumb
}));
const thisCommand = breadcrumb ? [{
breadcrumb,
noSlash: true,
preserveCase
}] : [];
const breadcrumbs = [...parentChain, ...thisCommand];
// generate the breadcrumb Elements from the model
const crumbs = yield promiseEach(breadcrumbs, makeBreadcrumb);
// attach the breadcrumb to the view
const container = div(undefined, 'bx--breadcrumb bx--breadcrumb--no-trailing-slash', 'h2');
result.appendChild(container);
crumbs.forEach(breadcrumb => container.appendChild(breadcrumb));
}
//
// title
//
/* if (title) {
const dom = div(title, 'capitalize', 'h1')
dom.style.fontSize = '1.629em'
dom.style.fontWeight = 300
dom.style.color = 'var(--color-brand-01)'
dom.style.margin = '0 0 .3rem'
result.appendChild(dom)
} */
//
// header message
//
if (header) {
let headerDiv = div(header, 'normal-text sans-serif');
if (!(0, _capabilities.isHeadless)()) {
try {
/* const renderer = new Marked.Renderer()
const marked = (_: string): string => Marked(_, { renderer })
renderer.link = (href: string, title: string, text: string) => {
return `<a class='bx--link' target='_blank' title="${title}" href="${href}">${text}</a>`
} */
headerDiv = div('', 'normal-text sans-serif marked-content');
headerDiv.innerText = header; // marked(header)
} catch (err) {
debug('error using marked', err);
}
}
if (message.exitCode > 0 && message.exitCode !== 200) {
// the underlying command emitted some sort of usage model in response to an error
// e.g. `kubectl get pods --bogus
headerDiv.classList.add('red-text');
}
result.appendChild(headerDiv);
}
body.style.display = 'flex';
body.style.flexWrap = 'wrap';
body.style.marginTop = '0.375em';
body.appendChild(left);
body.appendChild(right);
result.appendChild(body);
// keep track of the scroll regions we have created; if not
// many, maybe we use outer scrolling inside of inner scrolling
const scrollRegions = [];
// any minimally formatted sections? e.g. `intro` and `section` fields
const makeSection = (parent = right, noMargin = false) => ({
title,
content
}) => {
const wrapper = bodyPart(noMargin);
const prePart = prefix(title);
const contentPart = document.createElement('pre');
contentPart.classList.add('pre-wrap');
contentPart.innerText = content;
wrapper.appendChild(prePart);
wrapper.appendChild(contentPart);
parent.appendChild(wrapper);
};
// top-most intro section?
if (intro) {
makeSection(left)(intro);
}
// example command
if (example) {
const examplePart = bodyPart();
const prePart = prefix('Usage');
const html = example.trim().replace(/</g, '<').replace(/>/g, '>').replace(/(\[[^[\]]+\])/g, '<span class="even-lighter-text">$1</span>'); // lighter text for [optional args]
const textPart = div();
textPart.innerHTML = html;
left.appendChild(examplePart);
examplePart.appendChild(prePart);
examplePart.appendChild(textPart);
textPart.style.color = 'var(--color-base0C)';
}
// detailed example command
if (detailedExample) {
const examples = !Array.isArray(detailedExample) ? [detailedExample] : detailedExample;
const examplePart = bodyPart();
const prePart = prefix(examples.length === 1 ? 'Example' : 'Examples');
left.appendChild(examplePart);
examplePart.appendChild(prePart);
// only if we there are other sections to show, render the
// detailed examples in a scroll region
const examplesInScrollRegion = examples.length > 4 && sections && sections.length > 0;
const rowsPart = examplesInScrollRegion ? div(undefined, ['scrollable', 'scrollable-auto']) : examplePart;
if (examplesInScrollRegion) {
scrollRegions.push(rowsPart);
const nRowsInViewport = 6;
examplePart.appendChild(rowsPart);
rowsPart.style.maxHeight = `calc(${nRowsInViewport} * 3em + 1px)`;
}
examples.forEach(({
command,
docs
}) => {
const exampleContainer = div(undefined, 'present-as-quotation');
rowsPart.appendChild(exampleContainer);
const textPart = div(command);
const docPart = sans(div(docs, 'lighter-text'));
exampleContainer.appendChild(textPart);
exampleContainer.appendChild(docPart);
textPart.classList.add('example-command-text');
});
}
/**
* Render a table of options
*
*/
const makeTable = (title, rows, parent = right, nRowsInViewport = usage && usage.nRowsInViewport || 5) => {
const wrapper = bodyPart();
const prePart = prefix(title);
wrapper.appendChild(prePart);
parent.appendChild(wrapper);
if (typeof rows === 'string') {
// then we have a degenerate case where the "rows" is a
// plain string
debug('plain string body', rows);
const content = document.createElement('div');
content.innerText = rows;
wrapper.appendChild(content);
return;
}
const table = document.createElement('table');
table.className = 'log-lines';
// make the table scrollable, showing only a max number of
// rows at a time: e.g. 5 rows, plus a bit (1px) for the
// bottom border; the 3em part must .log-line's height in
// ui.css; nRowsInViewport = true means disable inner scrolling
if (rows.length > nRowsInViewport && nRowsInViewport !== true) {
const tableScrollable = div(undefined, ['scrollable', 'scrollable-auto']);
const nRows = sections && (sections.length === 2 || sections.length === 1 && scrollableDetailedExamples) ? 8 : nRowsInViewport;
tableScrollable.style.maxHeight = `calc(${nRows} * 3em + 1px)`;
tableScrollable.appendChild(table);
wrapper.appendChild(tableScrollable);
scrollRegions.push(tableScrollable);
} else {
wrapper.appendChild(table);
}
// render the rows
const renderRow = rowData => {
if (isGenerator(rowData)) {
// then rowData is a generator for aliases
renderRow(rowData.fn(rowData.command));
return;
}
const isDir = rowData.available !== undefined || false;
// fields of the row model
// debug('row', rowData)
const {
commandPrefix = outerCommandPrefix,
commandSuffix = outerCommandSuffix,
command = outerCommand,
name = command,
label = name,
noclick = false,
synonyms,
alias,
numeric,
aliases = (synonyms || [alias]).filter(x => x),
hidden = false,
advanced = false,
example = numeric && 'N',
title,
header,
docs = header || title,
allowed,
defaultValue
} = rowData;
// row is either hidden or only shown for advanced users
if (hidden) return;
if (advanced) return; // for now
const row = table.insertRow(-1);
row.className = 'log-line entity';
const cmdCell = row.insertCell(-1);
const docsCell = row.insertCell(-1);
const cmdPart = span(label && label.replace(/=/g, '=\u00ad'), 'pre-wrap');
const dirPart = isDir && label && span('/');
const examplePart = example && span(example, label || dirPart ? 'left-pad lighter-text smaller-text' : ''); // for -p key value, "key value"
const aliasesPart = aliases && aliases.length > 0 && span(undefined, 'lighter-text smaller-text small-left-pad');
const docsPart = span(docs);
const allowedPart = allowed && smaller(span(undefined));
// for repl.exec,
const commandForExec = (alias, cmd = '') => {
return `${commandPrefix && !commandPrefixNotNeeded ? commandPrefix + ' ' : ''}${alias} ${cmd} ${commandSuffix}`;
};
cmdCell.className = 'log-field';
docsCell.className = 'log-field';
cmdPart.style.fontWeight = '500';
wrap(sans(docsPart));
// command aliases
if (aliases) {
aliases.filter(x => x).forEach(alias => {
const cmdCell = span();
const cmdPart = span(alias /* noclick ? '' : 'clickable clickable-blatant' */); // don't make aliases clickable
const dirPart = isDir && span('/');
if (!noclick) {
cmdPart.onclick = pexec(commandForExec(alias));
}
aliasesPart.appendChild(cmdCell);
cmdCell.appendChild(cmdPart);
if (dirPart) cmdCell.appendChild(smaller(dirPart));
});
}
// allowed and default values
if (allowed) {
allowedPart.style.color = 'var(--color-text-02)';
allowedPart.appendChild(span('options: '));
allowed.forEach((value, idx) => {
const option = span(`${idx > 0 ? ', ' : ''}${value}${value !== defaultValue ? '' : '*'}`);
allowedPart.appendChild(option);
});
}
cmdCell.appendChild(cmdPart);
if (dirPart) cmdCell.appendChild(smaller(dirPart));
if (aliasesPart) cmdCell.appendChild(smaller(aliasesPart));
if (examplePart) cmdCell.appendChild(examplePart);
docsCell.appendChild(docsPart);
if (allowedPart) docsCell.appendChild(allowedPart);
if (command) {
if (!isDir) cmdPart.classList.add('semi-bold');
if (!noclick) {
cmdPart.classList.add('clickable');
cmdPart.classList.add('clickable-blatant');
cmdPart.setAttribute('tabindex', '0');
cmdPart.onclick = () => __awaiter(void 0, void 0, void 0, function* () {
return pexec(commandForExec(command, name !== command ? name : undefined))();
});
}
}
}; /* renderRow */
rows.forEach(renderRow);
return wrapper;
};
if (sections) {
// render more general sections, rather than the specific
// oneof, optional, etc.
const defaultNRowsInViewport = (idx, nRows) => {
if (idx === sections.length - 1 && sections.length % 2 === 0) {
return sections.length === 2 ? nRows : 5;
} // otherwise, accept global default
};
const stringSections = sections.filter(_ => typeof _.rows === 'string');
const tableSections = sections.filter(_ => Array.isArray(_.rows));
debug('string', stringSections);
debug('table', tableSections);
tableSections.sort(({
rows: a
}, {
rows: b
}) => a.length - b.length);
const nRowsOf = (section, idx) => {
debug('nRowsOf', section, section.rows.length, section.nRowsInViewport, defaultNRowsInViewport(idx, section.rows.length));
return Math.min(section.rows.length, section.nRowsInViewport || defaultNRowsInViewport(idx, section.rows.length) || section.rows.length);
};
const totalRows = tableSections.reduce((total, section, idx) => {
return total + nRowsOf(section, idx);
}, 0);
stringSections.forEach(section => {
const {
title,
rows
} = section;
makeTable(title, rows, left);
});
let runningSum = 0;
tableSections.forEach((section, idx) => {
const {
title,
rows,
nRowsInViewport
} = section;
runningSum += nRowsOf(section, idx);
debug('running', runningSum, totalRows);
makeTable(title, rows, runningSum < totalRows / 2 ? left : right, nRowsInViewport || defaultNRowsInViewport(idx, rows.length));
});
}
if (available) {
const table = makeTable('Available Commands', available);
table.classList.add('user-error-available-commands');
if (!title && !header) {
table.style.marginTop = '0';
}
}
if (required) {
makeTable('Required Parameters', required);
}
if (oneof) {
makeTable('Required Parameters (choose one of the following)', oneof);
}
if (optional) {
makeTable('Optional Parameters', optional);
}
if (sampleInputs) {
// attach this to the left side, along with usage
makeTable('Sample Inputs', sampleInputs, left);
}
if (related) {
const relatedPart = bodyPart();
const prePart = prefix('Related Commands');
const listPart = div();
relatedPart.appendChild(prePart);
relatedPart.appendChild(listPart);
result.appendChild(relatedPart); // note that we append to result not body; body is for the flex-wrap bits
related.forEach((command, idx) => {
const commandPart = span(undefined, '');
const commaPart = span(idx === 0 ? '' : ', ', '');
const clickablePart = span(command, 'clickable');
commandPart.appendChild(commaPart);
commandPart.appendChild(clickablePart);
clickablePart.onclick = pexec(command);
listPart.appendChild(commandPart);
});
}
// only one scroll region? then we might as well use outer scrolling
if (scrollRegions.length === 1) {
scrollRegions[0].classList.remove('scrollable');
scrollRegions[0].classList.remove('scrollable-auto');
scrollRegions[0].style.maxHeight = '';
}
return resultWrapper;
}
});
var _default = UsageError;
exports.default = _default;