Skip to content

Commit 52b7e5b

Browse files
feat: device summary + github issue creator
1 parent 937a5c0 commit 52b7e5b

File tree

6 files changed

+242
-14
lines changed

6 files changed

+242
-14
lines changed

package.json

+18-1
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,11 @@
358358
}
359359
],
360360
"commands": [
361+
{
362+
"command": "pymakr.debug.showDeviceSummary",
363+
"title": "Show device summary",
364+
"category": "PyMakr"
365+
},
361366
{
362367
"command": "pymakr.showDebugMenu",
363368
"title": "Shows debugging menu",
@@ -598,6 +603,10 @@
598603
"icon": "$(ellipsis)",
599604
"label": "device"
600605
},
606+
{
607+
"id": "pymakr.deviceMenuDebug",
608+
"label": "Debug"
609+
},
601610
{
602611
"id": "pymakr.projectMenu",
603612
"icon": "$(ellipsis)",
@@ -774,10 +783,18 @@
774783
"group": "4-config"
775784
},
776785
{
777-
"command": "pymakr.showTerminalHistory",
786+
"submenu": "pymakr.deviceMenuDebug",
778787
"group": "5-debug"
779788
}
780789
],
790+
"pymakr.deviceMenuDebug": [
791+
{
792+
"command": "pymakr.debug.showDeviceSummary"
793+
},
794+
{
795+
"command": "pymakr.showTerminalHistory"
796+
}
797+
],
781798
"pymakr.editorContextMenu": [
782799
{
783800
"command": "pymakr.runEditor",

src/commands/index.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,14 @@ class Commands {
3838
}
3939

4040
commands = {
41-
// debugPrintAdapterQueue: () => {
42-
// this.pymakr.devicesStore.get().forEach((device) => console.log(device.name, device.adapter.__proxyMeta));
43-
// },
41+
/**
42+
* @param {{device: Device}} param0
43+
*/
44+
"debug.showDeviceSummary": async ({ device }) => {
45+
let uri = vscode.Uri.parse("pymakrDocument:" + "Pymakr: device summary - " + device.raw.path);
46+
this.pymakr.textDocumentProvider.onDidChangeEmitter.fire(uri);
47+
vscode.commands.executeCommand("markdown.showPreview", uri);
48+
},
4449
showDebugMenu: async () => {
4550
const options = {
4651
"log adapter queue": "log adapter queue",

src/providers/TextDocumentProvider.js

+67-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
const { SerialPort } = require("serialport");
22
const vscode = require("vscode");
3+
const { timestamp } = require("../utils/createLogger");
4+
const { arraysToMarkdownTable, adapterHistoryTable } = require("../utils/formatters");
35
const { serializeKeyValuePairs } = require("../utils/misc");
46

7+
/**
8+
* @typedef {Object} subProvider
9+
* @prop {string|RegExp} match
10+
* @prop {(...matches:string[]) => Promise<string>|string} body
11+
*/
12+
513
/**
614
* @implements {vscode.TextDocumentContentProvider}
715
*/
@@ -14,19 +22,69 @@ class TextDocumentProvider {
1422
}
1523

1624
provideTextDocumentContent(uri) {
17-
const doc = this.paths[uri.path];
18-
if(typeof doc === 'undefined') throw new Error('could not find pymakr document: ' + uri.toString())
19-
return doc()
25+
const path = uri.path || uri;
26+
for (const subProvider of this.subProviders) {
27+
const matches = path.match(subProvider.match);
28+
if (matches) return subProvider.body(...matches);
29+
}
30+
31+
throw new Error("could not find pymakr document: " + uri.toString());
2032
}
2133

22-
paths = {
23-
'Pymakr: available devices': async () => {
24-
const devices = await SerialPort.list();
25-
const devicesStr = devices.map((e) => serializeKeyValuePairs(e)).join("\r\n\r\n");
34+
onDidChangeEmitter = new vscode.EventEmitter();
35+
onDidChange = this.onDidChangeEmitter.event;
36+
37+
/**
38+
* @type {subProvider[]}
39+
*/
40+
subProviders = [
41+
{
42+
match: "available devices",
43+
body: async () => {
44+
const devices = await SerialPort.list();
45+
const devicesStr = devices.map((e) => serializeKeyValuePairs(e)).join("\r\n\r\n");
46+
return devicesStr;
47+
},
48+
},
49+
{
50+
match: /device summary - (.+)/,
51+
body: (_all, path) => {
52+
const GithubMaxLengthUri = 8170; //8182 to be exact
53+
const device = this.pymakr.devicesStore.get().find((device) => device.raw.path === path);
54+
const config = { ...device.config, password: "***", username: "***" };
55+
const configTable = arraysToMarkdownTable([["Config", ""], ...Object.entries(config || {})]);
56+
const deviceTable = arraysToMarkdownTable([["Device", ""], ...Object.entries(device.raw || {})]);
57+
const systemTable = arraysToMarkdownTable([["System", ""], ...Object.entries(device.info || {})]);
58+
59+
const historyTable = arraysToMarkdownTable(adapterHistoryTable(device));
60+
const body = [
61+
configTable,
62+
deviceTable,
63+
systemTable,
64+
`## Device History at ${timestamp(new Date())}`,
65+
historyTable,
66+
].join("\r\n\r\n");
67+
68+
const intro = encodeURI(
69+
"What I did: ***\n\nWhat I expected to happen: ***\n\nWhat actually happened: ***\n\nAdditional info: ***\n\n<!--Device info below. Please check for passwords or sensitive info.-->\n\n"
70+
);
71+
const truncateMsg = encodeURIComponent("\n\n\n#### History was truncated");
72+
let url = `https://github.com/pycom/pymakr-vsc/issues/new?body=${intro + encodeURIComponent(body)}`;
73+
if (url.length > GithubMaxLengthUri) url = url.slice(0, GithubMaxLengthUri - truncateMsg.length) + truncateMsg;
74+
75+
const createIssueButton = [
76+
"# Create issue on Github",
77+
`### !WARNING ABOUT SENSITIVE DATA!`,
78+
`**Your device history may contain credentials or other sensitive data.**`,
79+
`After clicking the link below, please review the data before submitting it.`,
80+
`Pycom is not liable for credentials or other sensitive data posted to Github!`,
81+
`### [I understand - Create an issue on Github ](${url})`,
82+
].join("\r\n\r\n");
2683

27-
return devicesStr;
84+
return [body, "# <br />", createIssueButton].join("\r\n\r\n");
85+
},
2886
},
29-
};
87+
];
3088
}
3189

3290
module.exports = { TextDocumentProvider };

src/utils/createLogger.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,4 @@ const createLogger = (name) => {
8282
return log;
8383
};
8484

85-
module.exports = { createLogger };
85+
module.exports = { createLogger, timestamp };

src/utils/formatters.js

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @typedef {Object} TableOptions
3+
* @prop {(value: (string | number | boolean)) => (string | number | boolean)} tableWrap
4+
* @prop {(value: (string | number | boolean), row) => (string | number | boolean)} rowWrap
5+
* @prop {(value: (string | number | boolean), row, col) => (string | number | boolean)} colWrap
6+
* @prop {string} colDelim
7+
* @prop {string} rowDelim
8+
*/
9+
10+
const { timestamp } = require("./createLogger");
11+
12+
const sanitize = (str) =>
13+
(typeof str === "string" ? str : JSON.stringify(str, null, 2))
14+
.replace(/\|/gm, "&#124;")
15+
.replace(/\r?\n/gm, "<br />")
16+
.replace(/"username": ".+?"/, '"username": "&#42;&#42;&#42;"')
17+
.replace(/"password": ".+?"/, '"password": "&#42;&#42;&#42;"');
18+
const sanitizeTableValues = (table) => table.map((columns) => columns.map(sanitize));
19+
20+
const arraysToHTMLTable = (arrays, hasHeader) =>
21+
arraysToTable(arrays, {
22+
tableWrap: (value) => `<table>\n${value}\n</table>`,
23+
rowWrap: (value, row) => (hasHeader && !row ? `<th>${value}</th>` : `<tr>${value}</tr>`),
24+
colWrap: (value, row, col) => `<td>${value}</td>`,
25+
colDelim: " ",
26+
rowDelim: "\n",
27+
});
28+
29+
/** @param {(string | number | boolean)[][]} rows */
30+
const arraysToMarkdownTable = ([...rows]) => {
31+
rows.splice(1, 0, Array(rows[0].length).fill("-"));
32+
33+
return arraysToTable(rows, {
34+
tableWrap: (value) => value,
35+
rowWrap: (value, row) => `|${value}|`,
36+
colWrap: (value, row, col) => value,
37+
colDelim: "|",
38+
rowDelim: "\n",
39+
});
40+
};
41+
42+
/** @param {(string | number | boolean)[][]} rows */
43+
const arraysToTerminalTable = (rows) => {
44+
// determine max length of each column
45+
const colLenghts = Array(rows[0].length).fill(0);
46+
rows.forEach((row) =>
47+
row.forEach((col, index) => (colLenghts[index] = Math.max(col.toString().length, colLenghts[index])))
48+
);
49+
50+
return arraysToTable(rows, {
51+
tableWrap: (value) => value,
52+
rowWrap: (value, row) => value,
53+
colWrap: (value, row, col) => value.toString().padEnd(colLenghts[col]),
54+
colDelim: "|",
55+
rowDelim: "\n",
56+
});
57+
};
58+
59+
/**
60+
*
61+
* @param {(string | number | boolean)[][]} arrays
62+
* @param {TableOptions} options
63+
*/
64+
const arraysToTable = (arrays, options) => {
65+
arrays = sanitizeTableValues(arrays);
66+
const { tableWrap, rowWrap, colWrap, rowDelim, colDelim } = options;
67+
const formattedColumns = arrays.map((array, row) =>
68+
array.map((value, column) => colWrap(value, row, column)).join(colDelim)
69+
);
70+
const formattedRows = formattedColumns.map((columnStr, row) => rowWrap(columnStr, row)).join(rowDelim);
71+
72+
return tableWrap(formattedRows);
73+
};
74+
75+
const unBuffer = (str) =>
76+
str instanceof Buffer ? str.toString() : typeof str === "string" ? str : JSON.stringify(str, null, 2);
77+
78+
/**
79+
* @param {Device} device
80+
*/
81+
const adapterHistoryTable = (device) => {
82+
const fields = ["function", "args", "time", "result", "queued at", "finished at", "failed"];
83+
console.log("proxymeta", device.adapter.__proxyMeta);
84+
const rows = device.adapter.__proxyMeta.history.map((entry) => [
85+
entry.field.toString(),
86+
entry.args.length ? `<details> <pre>${unBuffer(entry.args)}</pre> </details>` : " ",
87+
entry.runDuration,
88+
`<details> <pre>${entry.error || unBuffer(entry.result)}</pre> </details>`,
89+
entry.queuedAt && timestamp(entry.queuedAt),
90+
entry.finishedAt && timestamp(entry.finishedAt),
91+
!!entry.error,
92+
]);
93+
return [fields, ...rows.reverse()];
94+
};
95+
96+
module.exports = { arraysToHTMLTable, arraysToMarkdownTable, arraysToTerminalTable, adapterHistoryTable };

src/utils/specs/formatters.test.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const { arraysToHTMLTable, arraysToMarkdownTable, arraysToTerminalTable } = require("../formatters");
2+
3+
test("can create tables from arrays", () => {
4+
const data = [
5+
["header1", "header2", "header3"],
6+
["cola1", "cola2", "cola3"],
7+
["colb1", "colb2", "colb3"],
8+
];
9+
10+
test("can create HTML table", () => {
11+
const output = arraysToHTMLTable(data);
12+
assert.equal(
13+
output,
14+
`<table>
15+
<tr><td>header1</td> <td>header2</td> <td>header3</td></tr>
16+
<tr><td>cola1</td> <td>cola2</td> <td>cola3</td></tr>
17+
<tr><td>colb1</td> <td>colb2</td> <td>colb3</td></tr>
18+
</table>`
19+
);
20+
});
21+
test("can create HTML table with header", () => {
22+
const output = arraysToHTMLTable(data, true);
23+
assert.equal(
24+
output,
25+
`<table>
26+
<th><td>header1</td> <td>header2</td> <td>header3</td></th>
27+
<tr><td>cola1</td> <td>cola2</td> <td>cola3</td></tr>
28+
<tr><td>colb1</td> <td>colb2</td> <td>colb3</td></tr>
29+
</table>`
30+
);
31+
});
32+
test("can create markdown table", () => {
33+
const output = arraysToMarkdownTable(data);
34+
assert.equal(
35+
output,
36+
`|header1|header2|header3|
37+
|-|-|-|
38+
|cola1|cola2|cola3|
39+
|colb1|colb2|colb3|`
40+
);
41+
});
42+
test("can create terminal table", () => {
43+
const output = arraysToTerminalTable(data);
44+
console.log(output);
45+
assert.equal(
46+
output,
47+
`header1|header2|header3
48+
cola1 |cola2 |cola3
49+
colb1 |colb2 |colb3 `
50+
);
51+
});
52+
});

0 commit comments

Comments
 (0)