diff --git a/package-lock.json b/package-lock.json index f0b16c2..d2b97aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,8 @@ "css-loader": "^5.0.0", "mobx": "^6.3.0", "mobx-react": "^7.2.0", + "moment": "^2.29.3", + "moment-timezone": "^0.5.34", "sass": "^1.29.0", "sass-loader": "^10.0.0", "style-loader": "^2.0.0", @@ -2561,6 +2563,27 @@ } } }, + "node_modules/moment": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.34", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz", + "integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==", + "dev": true, + "dependencies": { + "moment": ">= 2.9.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5807,6 +5830,21 @@ "dev": true, "requires": {} }, + "moment": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "dev": true + }, + "moment-timezone": { + "version": "0.5.34", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz", + "integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==", + "dev": true, + "requires": { + "moment": ">= 2.9.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index fe2e9ce..e2c251d 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ "css-loader": "^5.0.0", "mobx": "^6.3.0", "mobx-react": "^7.2.0", + "moment": "^2.29.3", + "moment-timezone": "^0.5.34", "sass": "^1.29.0", "sass-loader": "^10.0.0", "style-loader": "^2.0.0", diff --git a/src/ciskubebenchreports/page.scss b/src/ciskubebenchreports/page.scss index f322cd0..e23272a 100644 --- a/src/ciskubebenchreports/page.scss +++ b/src/ciskubebenchreports/page.scss @@ -9,11 +9,14 @@ flex-grow: 0.45; } &.summary { - flex-grow: 0.4; + flex-grow: 0.35; } &.scanner { flex-grow: 0.15; } + &.age { + flex-grow: 0.05; + } } } diff --git a/src/ciskubebenchreports/page.tsx b/src/ciskubebenchreports/page.tsx index 708dd05..e83c0ad 100644 --- a/src/ciskubebenchreports/page.tsx +++ b/src/ciskubebenchreports/page.tsx @@ -3,6 +3,7 @@ import {Renderer} from "@k8slens/extensions"; import React from "react"; import {store} from "./store"; import {CISKubeBenchReport} from "./types"; +import formatDuration from "../utils/formatDuration"; const { Component: { @@ -14,6 +15,7 @@ const { enum sortBy { name = "name", summary = "summary", + age = "age", } export class CISKubeBenchReportsPage extends React.Component<{ extension: Renderer.LensExtension }> { @@ -30,7 +32,8 @@ export class CISKubeBenchReportsPage extends React.Component<{ extension: Render report.report.summary.infoCount, report.report.summary.passCount, report.report.summary.warnCount, - ] + ], + [sortBy.age]: (report: CISKubeBenchReport) => report.metadata.creationTimestamp, }} searchFilters={[ (report: CISKubeBenchReport) => report.getSearchFields() @@ -40,6 +43,7 @@ export class CISKubeBenchReportsPage extends React.Component<{ extension: Render {title: "Name", className: "name", sortBy: sortBy.name}, {title: "Summary", className: "summary", sortBy: sortBy.summary}, {title: "Scanner", className: "scanner"}, + {title: "Age", className: "age", sortBy: sortBy.age}, ]} renderTableContents={(report: CISKubeBenchReport) => [ report.getName(), @@ -50,6 +54,7 @@ export class CISKubeBenchReportsPage extends React.Component<{ extension: Render renderResult("pass", report.report.summary.passCount), ], report.report.scanner.name + " " + report.report.scanner.version, + renderAge(report.metadata.creationTimestamp), ]} /> ) @@ -66,4 +71,13 @@ function renderResult(result: string, count: number) { ) } +} + +function renderAge(timestamp: string) { + const creationTimestamp = new Date(timestamp).getTime(); + return ( + + ) } \ No newline at end of file diff --git a/src/configauditreports/page.scss b/src/configauditreports/page.scss index aa3fbf3..20ddda6 100644 --- a/src/configauditreports/page.scss +++ b/src/configauditreports/page.scss @@ -9,13 +9,16 @@ flex-grow: 0.45; } &.namespace { - flex-grow: 0.1; + flex-grow: 0.075; } &.summary { flex-grow: 0.3; } &.scanner { - flex-grow: 0.15; + flex-grow: 0.125; + } + &.age { + flex-grow: 0.05; } } } diff --git a/src/configauditreports/page.tsx b/src/configauditreports/page.tsx index 1c82f3c..cce80f0 100644 --- a/src/configauditreports/page.tsx +++ b/src/configauditreports/page.tsx @@ -3,6 +3,7 @@ import {Renderer} from "@k8slens/extensions"; import React from "react"; import {clusterStore, store} from "./store"; import {ClusterConfigAuditReport, ConfigAuditReport} from "./types"; +import formatDuration from "../utils/formatDuration"; const { Component: { @@ -15,7 +16,8 @@ enum sortBy { name = "name", namespace = "namespace", summary = "summary", - scanner = "scanner" + scanner = "scanner", + age = "age", } export class ClusterConfigAuditReportPage extends React.Component<{ extension: Renderer.LensExtension }> { @@ -34,6 +36,7 @@ export class ClusterConfigAuditReportPage extends React.Component<{ extension: R report.report.summary.lowCount ], [sortBy.scanner]: (report: ClusterConfigAuditReport) => report.report.scanner.name + " " + report.report.scanner.version, + [sortBy.age]: (report: ConfigAuditReport) => report.metadata.creationTimestamp, }} searchFilters={[ (report: ClusterConfigAuditReport) => report.getSearchFields() @@ -43,6 +46,7 @@ export class ClusterConfigAuditReportPage extends React.Component<{ extension: R {title: "Name", sortBy: sortBy.name}, {title: "Summary", className: "summary", sortBy: sortBy.summary}, {title: "Scanner", sortBy: sortBy.scanner}, + {title: "Age", className: "age", sortBy: sortBy.age}, ]} renderTableContents={(report: ClusterConfigAuditReport) => [ renderName(report.getName()), @@ -53,6 +57,7 @@ export class ClusterConfigAuditReportPage extends React.Component<{ extension: R renderSeverity("LOW", report.report.summary.lowCount), ], report.report.scanner.name + " " + report.report.scanner.version, + renderAge(report.metadata.creationTimestamp), ]} /> ) @@ -76,6 +81,7 @@ export class ConfigAuditReportPage extends React.Component<{ extension: Renderer report.report.summary.lowCount ], [sortBy.scanner]: (report: ConfigAuditReport) => report.report.scanner.name + " " + report.report.scanner.version, + [sortBy.age]: (report: ConfigAuditReport) => report.metadata.creationTimestamp, }} searchFilters={[ (report: ConfigAuditReport) => report.getSearchFields() @@ -86,6 +92,7 @@ export class ConfigAuditReportPage extends React.Component<{ extension: Renderer {title: "Namespace", className: "namespace", sortBy: sortBy.namespace}, {title: "Summary", className: "summary", sortBy: sortBy.summary}, {title: "Scanner", className: "scanner", sortBy: sortBy.scanner}, + {title: "Age", className: "age", sortBy: sortBy.age}, ]} renderTableContents={(report: ConfigAuditReport) => [ renderName(report.getName()), @@ -97,6 +104,7 @@ export class ConfigAuditReportPage extends React.Component<{ extension: Renderer renderSeverity("LOW", report.report.summary.lowCount), ], report.report.scanner.name + " " + report.report.scanner.version, + renderAge(report.metadata.creationTimestamp), ]} /> ) @@ -119,4 +127,13 @@ function renderSeverity(severity: string, count: number) { ) } +} + +function renderAge(timestamp: string) { + const creationTimestamp = new Date(timestamp).getTime(); + return ( + + ) } \ No newline at end of file diff --git a/src/utils/formatDuration.ts b/src/utils/formatDuration.ts new file mode 100644 index 0000000..d600e33 --- /dev/null +++ b/src/utils/formatDuration.ts @@ -0,0 +1,91 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + + import moment from "moment"; + + /** + * This function formats durations in a more human readable form. + * @param timeValue the duration in milliseconds to format + */ + export default function formatDuration(timeValue: number, compact = true) { + const duration = moment.duration(timeValue, "milliseconds"); + const seconds = Math.floor(duration.asSeconds()); + const separator = compact ? "": " "; + + if (seconds < 0) { + return "0s"; + } else if (seconds < 60*2 ) { + return `${seconds}s`; + } + + const minutes = Math.floor(duration.asMinutes()); + + if (minutes < 10) { + const seconds = duration.seconds(); + + return getMeaningfulValues([minutes, seconds], ["m", "s"], separator); + } else if (minutes < 60 * 3) { + if (!compact) { + return getMeaningfulValues([minutes, duration.seconds()], ["m", "s"]); + } + + return `${minutes}m`; + } + + const hours = Math.floor(duration.asHours()); + + if(hours < 8) { + const minutes = duration.minutes(); + + return getMeaningfulValues([hours, minutes], ["h", "m"], separator); + } else if (hours < 48) { + if (compact) { + return `${hours}h`; + } else { + return getMeaningfulValues([hours, duration.minutes()], ["h", "m"]); + } + } + + const days = Math.floor(duration.asDays()); + + if (days < 8) { + const hours = duration.hours(); + + if (compact) { + return getMeaningfulValues([days, hours], ["d", "h"], separator); + } else { + return getMeaningfulValues([days, hours, duration.minutes()], ["d", "h", "m"]); + } + } + const years = Math.floor(duration.asYears()); + + if (years < 2) { + if (compact) { + return `${days}d`; + } else { + return getMeaningfulValues([days, duration.hours(), duration.minutes()], ["d", "h", "m"]); + } + } else if (years < 8) { + const days = duration.days(); + + if (compact) { + return getMeaningfulValues([years, days], ["y", "d"], separator); + } + } + + if (compact) { + return `${years}y`; + } + + return getMeaningfulValues([years, duration.days(), duration.hours(), duration.minutes()], ["y", "d", "h", "m"]); + } + + function getMeaningfulValues(values: number[], suffixes: string[], separator = " ") { + return values + .map((a, i): [number, string] => [a, suffixes[i]]) + .filter(([dur]) => dur > 0) + .map(([dur, suf]) => dur + suf) + .join(separator); + } \ No newline at end of file diff --git a/src/vulnerabilityreports/page.scss b/src/vulnerabilityreports/page.scss index 04f2e04..97753ff 100644 --- a/src/vulnerabilityreports/page.scss +++ b/src/vulnerabilityreports/page.scss @@ -9,7 +9,7 @@ flex-grow: 0.25; } &.namespace { - flex-grow: 0.1; + flex-grow: 0.075; } &.repository { flex-grow: 0.25; @@ -18,7 +18,10 @@ flex-grow: 0.3; } &.scanner { - flex-grow: 0.1; + flex-grow: 0.075; + } + &.age { + flex-grow: 0.05; } } } diff --git a/src/vulnerabilityreports/page.tsx b/src/vulnerabilityreports/page.tsx index 92f9ef0..f1bc9a3 100644 --- a/src/vulnerabilityreports/page.tsx +++ b/src/vulnerabilityreports/page.tsx @@ -4,6 +4,7 @@ import React from "react"; import {ClusterVulnerabilityReport, VulnerabilityReport} from "./types"; import {clusterStore, store} from "./store"; import {Scanner} from "../starboard/types"; +import formatDuration from "../utils/formatDuration"; const { Component: { @@ -16,6 +17,7 @@ enum sortBy { name = "name", namespace = "namespace", summary = "summary", + age = "age", } export class ClusterVulnerabilityReportPage extends React.Component<{ extension: Renderer.LensExtension }> { @@ -34,6 +36,7 @@ export class ClusterVulnerabilityReportPage extends React.Component<{ extension: report.report.summary.lowCount, report.report.summary.unknownCount, ], + [sortBy.age]: (report: ClusterVulnerabilityReport) => report.metadata.creationTimestamp, }} searchFilters={[ (report: ClusterVulnerabilityReport) => report.getSearchFields() @@ -44,6 +47,7 @@ export class ClusterVulnerabilityReportPage extends React.Component<{ extension: {title: "Image", className: "repository"}, {title: "Summary", className: "summary", sortBy: sortBy.summary}, {title: "Scanner", className: "scanner"}, + {title: "Age", className: "age", sortBy: sortBy.age}, ]} renderTableContents={(report: ClusterVulnerabilityReport) => [ renderName(report.getName()), @@ -56,6 +60,7 @@ export class ClusterVulnerabilityReportPage extends React.Component<{ extension: renderSeverity("UNKNOWN", report.report.summary.unknownCount), ], renderScanner(report.report.scanner), + renderAge(report.metadata.creationTimestamp), ]} /> ) @@ -80,6 +85,7 @@ export class VulnerabilityReportPage extends React.Component<{ extension: Render report.report.summary.lowCount, report.report.summary.unknownCount, ], + [sortBy.age]: (report: VulnerabilityReport) => report.metadata.creationTimestamp, }} searchFilters={[ (report: VulnerabilityReport) => report.getSearchFields() @@ -91,6 +97,7 @@ export class VulnerabilityReportPage extends React.Component<{ extension: Render {title: "Image", className: "repository"}, {title: "Summary", className: "summary", sortBy: sortBy.summary}, {title: "Scanner", className: "scanner"}, + {title: "Age", className: "age", sortBy: sortBy.age}, ]} renderTableContents={(report: VulnerabilityReport) => [ renderName(report.getName()), @@ -104,6 +111,7 @@ export class VulnerabilityReportPage extends React.Component<{ extension: Render renderSeverity("UNKNOWN", report.report.summary.unknownCount), ], renderScanner(report.report.scanner), + renderAge(report.metadata.creationTimestamp), ]} /> ) @@ -138,3 +146,12 @@ function renderSeverity(severity: string, count: number) { ) } } + +function renderAge(timestamp: string) { + const creationTimestamp = new Date(timestamp).getTime(); + return ( + + ) +}