diff --git a/package-lock.json b/package-lock.json index 0fd329e..b3f36c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,13 @@ "dependencies": { "@vercel/speed-insights": "^2.0.0", "lit": "^3.3.2", + "markdown-it": "^14.1.1", "navigo": "^8.11.1" }, "devDependencies": { "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.2.1", + "@types/markdown-it": "^14.1.2", "rollup-plugin-minify-template-literals": "^1.1.7", "typescript": "^5.9.3", "vite": "^7.3.1" @@ -1204,6 +1206,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -1261,6 +1288,12 @@ "node": ">=0.4.0" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -1354,7 +1387,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -1745,6 +1777,15 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/lit": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", @@ -1796,6 +1837,29 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/minify-literals": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/minify-literals/-/minify-literals-1.0.10.tgz", @@ -1957,6 +2021,15 @@ "node": ">=4" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -2142,6 +2215,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 3100c65..dc2bdbe 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "devDependencies": { "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.2.1", + "@types/markdown-it": "^14.1.2", "rollup-plugin-minify-template-literals": "^1.1.7", "typescript": "^5.9.3", "vite": "^7.3.1" @@ -29,6 +30,7 @@ "dependencies": { "@vercel/speed-insights": "^2.0.0", "lit": "^3.3.2", + "markdown-it": "^14.1.1", "navigo": "^8.11.1" } } diff --git a/src/components/AppRoot.ts b/src/components/AppRoot.ts index 885b593..c60c622 100644 --- a/src/components/AppRoot.ts +++ b/src/components/AppRoot.ts @@ -116,6 +116,7 @@ export class AppRoot extends Component { return new ServiceGroup( c.id, c.name.default, + c.description.default === "" ? null : c.description.default, await Promise.all( c.children .sort(Services.sort) @@ -135,6 +136,7 @@ export class AppRoot extends Component { return new Service( c.id, c.name.default, + c.description.default === "" ? null : c.description.default, Service.parseStatus(c.status), await Promise.all( c.metrics.map(async (m) => { diff --git a/src/components/ServiceRow.ts b/src/components/ServiceRow.ts index 5fea621..6a64527 100644 --- a/src/components/ServiceRow.ts +++ b/src/components/ServiceRow.ts @@ -1,5 +1,7 @@ import { html, nothing, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; +import { unsafeHTML } from "lit/directives/unsafe-html.js"; +import markdownit from "markdown-it"; import { Component } from "./Component"; import { Service } from "../models/Service"; import { ServiceStatus } from "../models/ServiceStatus"; @@ -7,6 +9,8 @@ import { Notice } from "../models/Notice"; @customElement("service-row") export class ServiceRow extends Component { + private static readonly MD = markdownit(); + protected static readonly STATUS_STYLES: Record< ServiceStatus, { color: string; bar: string; label: string; icon: string } @@ -137,9 +141,33 @@ export class ServiceRow extends Component { protected renderTop(): TemplateResult { return html`
-
- ${this.renderIcon()} -

${this.service.name}

+
+
+ ${this.renderIcon()} +

${this.service.name}

+
+ ${this.service.description === null ? nothing : html` +
+ +
+ ${unsafeHTML( + ServiceRow.MD.render(this.service.description), + )} +
+
+ `}
${this.service.metrics.map((metric) => diff --git a/src/models/Service.ts b/src/models/Service.ts index 6977300..b7d245c 100644 --- a/src/models/Service.ts +++ b/src/models/Service.ts @@ -4,6 +4,7 @@ import { Metric } from "./Metric"; export class Service { public readonly id: string; public readonly name: string; + public readonly description: string | null; public readonly status: ServiceStatus; public readonly metrics: Metric[]; public readonly started: Date | null; @@ -12,6 +13,7 @@ export class Service { public constructor( id: string, name: string, + description: string | null, status: ServiceStatus, metrics: Metric[], started: Date | null, @@ -19,6 +21,7 @@ export class Service { ) { this.id = id; this.name = name; + this.description = description; this.status = status; this.metrics = metrics; this.started = started; diff --git a/src/models/ServiceGroup.ts b/src/models/ServiceGroup.ts index 16e6a51..107c596 100644 --- a/src/models/ServiceGroup.ts +++ b/src/models/ServiceGroup.ts @@ -8,6 +8,7 @@ export class ServiceGroup extends Service { public constructor( id: string, name: string, + description: string | null, children: Service[], showUptime: boolean, isCollapsed: boolean, @@ -15,6 +16,7 @@ export class ServiceGroup extends Service { super( id, name, + description, Services.mostSevere(children).status, [], Array.from(children).sort((a, b) =>