245 lines
8.6 KiB
JavaScript
245 lines
8.6 KiB
JavaScript
// Package documentation — turn an Intune deployment (+ optional linked PSADT
|
||
// profile and catalog application) into an auto-generated datasheet. Pure: it
|
||
// renders a section model to Markdown or self-contained HTML with no IO. The
|
||
// controller loads the records and picks the format.
|
||
|
||
const RETURN_CODE_LABELS = {
|
||
success: 'Success',
|
||
softReboot: 'Soft reboot',
|
||
hardReboot: 'Hard reboot',
|
||
retry: 'Retry',
|
||
failed: 'Failed'
|
||
};
|
||
|
||
const INTENT_LABELS = {
|
||
available: 'Available',
|
||
required: 'Required',
|
||
uninstall: 'Uninstall',
|
||
exclude: 'Excluded'
|
||
};
|
||
|
||
function dash(value) {
|
||
const str = value == null ? '' : String(value).trim();
|
||
return str === '' ? '—' : str;
|
||
}
|
||
|
||
// Build a format-neutral section model from the loaded records. Each section is
|
||
// one of: { rows } definition list, { table } grid, { code } block, or { text }.
|
||
export function buildDatasheetModel({ deployment, profile = null, application = null } = {}) {
|
||
if (!deployment) throw new Error('deployment is required');
|
||
|
||
const title = deployment.name || profile?.name || 'PSADT package';
|
||
const vendor = profile?.appVendor || application?.vendor || '';
|
||
const appName = profile?.appName || deployment.name || '';
|
||
const version = profile?.appVersion || application?.version || '';
|
||
const arch = profile?.appArch || deployment.requirements?.architecture || '';
|
||
const subtitle = [vendor, appName, version].map((part) => String(part || '').trim()).filter(Boolean).join(' ');
|
||
|
||
const sections = [];
|
||
|
||
sections.push({
|
||
heading: 'Overview',
|
||
rows: [
|
||
['Package', title],
|
||
['Vendor', vendor],
|
||
['Application', appName],
|
||
['Version', version],
|
||
['Architecture', arch],
|
||
['App type', deployment.appType],
|
||
['Command style', deployment.commandStyle],
|
||
['Install behavior', deployment.installBehavior],
|
||
['Restart behavior', deployment.restartBehavior],
|
||
['Status', deployment.status]
|
||
]
|
||
});
|
||
|
||
sections.push({
|
||
heading: 'Install & uninstall',
|
||
rows: [
|
||
['Install command', { code: deployment.installCommand }],
|
||
['Uninstall command', { code: deployment.uninstallCommand }]
|
||
]
|
||
});
|
||
|
||
sections.push({
|
||
heading: 'Detection',
|
||
rows: [['Detection type', deployment.detectionType || 'manual']],
|
||
code: deployment.detectionRule ? deployment.detectionRule : null
|
||
});
|
||
|
||
const returnCodes = Array.isArray(deployment.returnCodes) ? deployment.returnCodes : [];
|
||
sections.push({
|
||
heading: 'Return codes',
|
||
table: {
|
||
columns: ['Code', 'Type', 'Meaning'],
|
||
rows: returnCodes.length
|
||
? returnCodes.map((rc) => [String(rc.code), RETURN_CODE_LABELS[rc.type] || rc.type || '', rc.meaning || ''])
|
||
: []
|
||
}
|
||
});
|
||
|
||
const assignments = Array.isArray(deployment.assignments) ? deployment.assignments : [];
|
||
sections.push({
|
||
heading: 'Assignments',
|
||
table: {
|
||
columns: ['Ring', 'Intent', 'Group', 'Notes'],
|
||
rows: assignments.length
|
||
? assignments.map((a) => [a.ring || '', INTENT_LABELS[a.intent] || a.intent || '', a.groupName || '', a.notes || ''])
|
||
: []
|
||
}
|
||
});
|
||
|
||
const req = deployment.requirements || {};
|
||
sections.push({
|
||
heading: 'Requirements',
|
||
rows: [
|
||
['Architecture', req.architecture],
|
||
['Minimum OS', req.minOs],
|
||
['Disk space (MB)', req.diskSpaceMb ? String(req.diskSpaceMb) : ''],
|
||
['Run as 32-bit', req.runAs32Bit ? 'Yes' : 'No']
|
||
]
|
||
});
|
||
|
||
sections.push({
|
||
heading: 'Source & version',
|
||
rows: [
|
||
['PSADT profile', profile?.name || deployment.profileName || ''],
|
||
['Source folder', deployment.sourceFolder],
|
||
['Package file', deployment.intunewinFile],
|
||
['Catalog application', application?.name || deployment.applicationName || ''],
|
||
['Version source', application ? `${application.versionSource || ''} ${application.versionSourceRef || ''}`.trim() : '']
|
||
]
|
||
});
|
||
|
||
if (deployment.notes) {
|
||
sections.push({ heading: 'Notes', text: deployment.notes });
|
||
}
|
||
|
||
return { title, subtitle, generatedAt: new Date().toISOString(), sections };
|
||
}
|
||
|
||
function rowValueToMd(value) {
|
||
if (value && typeof value === 'object' && 'code' in value) return `\`${dash(value.code).replace(/`/g, '`')}\``;
|
||
return dash(value);
|
||
}
|
||
|
||
function renderMarkdown(model) {
|
||
const lines = [`# ${model.title}`, ''];
|
||
if (model.subtitle) lines.push(`*${model.subtitle}*`, '');
|
||
lines.push(`Generated ${model.generatedAt}`, '');
|
||
|
||
for (const section of model.sections) {
|
||
lines.push(`## ${section.heading}`, '');
|
||
if (section.rows) {
|
||
lines.push('| Field | Value |', '| --- | --- |');
|
||
for (const [label, value] of section.rows) {
|
||
lines.push(`| ${label} | ${rowValueToMd(value).replace(/\n/g, ' ')} |`);
|
||
}
|
||
lines.push('');
|
||
}
|
||
if (section.table) {
|
||
const { columns, rows } = section.table;
|
||
lines.push(`| ${columns.join(' | ')} |`, `| ${columns.map(() => '---').join(' | ')} |`);
|
||
if (rows.length) {
|
||
for (const row of rows) lines.push(`| ${row.map((cell) => dash(cell).replace(/\n/g, ' ').replace(/\|/g, '\\|')).join(' | ')} |`);
|
||
} else {
|
||
lines.push(`| ${columns.map((_, i) => (i === 0 ? '_None_' : '')).join(' | ')} |`);
|
||
}
|
||
lines.push('');
|
||
}
|
||
if (section.code) {
|
||
lines.push('```', section.code, '```', '');
|
||
}
|
||
if (section.text) {
|
||
lines.push(section.text, '');
|
||
}
|
||
}
|
||
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
||
}
|
||
|
||
function esc(value) {
|
||
return dash(value)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
function rowValueToHtml(value) {
|
||
if (value && typeof value === 'object' && 'code' in value) return `<code>${esc(value.code)}</code>`;
|
||
return esc(value);
|
||
}
|
||
|
||
function renderHtml(model) {
|
||
const parts = [];
|
||
for (const section of model.sections) {
|
||
parts.push(`<section><h2>${esc(section.heading)}</h2>`);
|
||
if (section.rows) {
|
||
parts.push('<table class="kv"><tbody>');
|
||
for (const [label, value] of section.rows) {
|
||
parts.push(`<tr><th>${esc(label)}</th><td>${rowValueToHtml(value)}</td></tr>`);
|
||
}
|
||
parts.push('</tbody></table>');
|
||
}
|
||
if (section.table) {
|
||
const { columns, rows } = section.table;
|
||
parts.push('<table class="grid"><thead><tr>');
|
||
parts.push(columns.map((col) => `<th>${esc(col)}</th>`).join(''));
|
||
parts.push('</tr></thead><tbody>');
|
||
if (rows.length) {
|
||
for (const row of rows) {
|
||
parts.push('<tr>' + row.map((cell) => `<td>${esc(cell)}</td>`).join('') + '</tr>');
|
||
}
|
||
} else {
|
||
parts.push(`<tr><td class="muted" colspan="${columns.length}">None configured</td></tr>`);
|
||
}
|
||
parts.push('</tbody></table>');
|
||
}
|
||
if (section.code) {
|
||
parts.push(`<pre><code>${esc(section.code)}</code></pre>`);
|
||
}
|
||
if (section.text) {
|
||
parts.push(`<p>${esc(section.text)}</p>`);
|
||
}
|
||
parts.push('</section>');
|
||
}
|
||
|
||
return [
|
||
'<!doctype html>',
|
||
'<html lang="en"><head><meta charset="utf-8">',
|
||
`<title>${esc(model.title)} — package datasheet</title>`,
|
||
'<style>',
|
||
':root{color-scheme:light dark}',
|
||
'body{font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:60rem;margin:2rem auto;padding:0 1rem;line-height:1.5}',
|
||
'h1{margin-bottom:.25rem}.subtitle{color:#6b7280;margin-top:0}.meta{color:#6b7280;font-size:.85rem}',
|
||
'section{margin-top:1.75rem}h2{border-bottom:1px solid #d1d5db;padding-bottom:.25rem}',
|
||
'table{border-collapse:collapse;width:100%;margin-top:.5rem}',
|
||
'th,td{text-align:left;padding:.4rem .6rem;border:1px solid #d1d5db;vertical-align:top}',
|
||
'table.kv th{width:14rem;white-space:nowrap}',
|
||
'code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em}',
|
||
'pre{background:#f3f4f6;padding:.75rem;border-radius:.4rem;overflow:auto}pre code{font-size:.85em}',
|
||
'.muted{color:#6b7280}',
|
||
'</style></head><body>',
|
||
`<h1>${esc(model.title)}</h1>`,
|
||
model.subtitle ? `<p class="subtitle">${esc(model.subtitle)}</p>` : '',
|
||
`<p class="meta">Generated ${esc(model.generatedAt)}</p>`,
|
||
parts.join('\n'),
|
||
'</body></html>',
|
||
''
|
||
].filter((line) => line !== '').join('\n');
|
||
}
|
||
|
||
// Render a datasheet for a deployment. format: 'md' (default) | 'html'.
|
||
export function renderDatasheet(records, { format = 'md' } = {}) {
|
||
const model = buildDatasheetModel(records);
|
||
const normalized = format === 'html' ? 'html' : 'md';
|
||
const content = normalized === 'html' ? renderHtml(model) : renderMarkdown(model);
|
||
return {
|
||
format: normalized,
|
||
contentType: normalized === 'html' ? 'text/html; charset=utf-8' : 'text/markdown; charset=utf-8',
|
||
extension: normalized === 'html' ? 'html' : 'md',
|
||
title: model.title,
|
||
content
|
||
};
|
||
}
|