Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion i18n/english.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,14 @@ const ui = {
deps: "deps",
direct: "direct",
modeDepth: "Depth",
modeTree: "Tree"
modeTree: "Tree",
modeActivity: "Activity",
activityFresh: "< 1 week",
activityRecent: "< 1 month",
activityActive: "< 6 months",
activityStable: "< 1 year",
activitySlow: "< 2 years",
activityStale: "Stale"
},
search_command: {
placeholder: "Search packages...",
Expand Down
9 changes: 8 additions & 1 deletion i18n/french.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,14 @@ const ui = {
deps: "dépendances",
direct: "directes",
modeDepth: "Profondeur",
modeTree: "Arbre"
modeTree: "Arbre",
modeActivity: "Activité",
activityFresh: "< 1 semaine",
activityRecent: "< 1 mois",
activityActive: "< 6 mois",
activityStable: "< 1 an",
activitySlow: "< 2 ans",
activityStale: "Abandonné"
},
search_command: {
placeholder: "Rechercher des packages...",
Expand Down
48 changes: 47 additions & 1 deletion public/components/views/tree/tree-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@ import prettyBytes from "pretty-bytes";

// Import Internal Dependencies
import { EVENTS } from "../../../core/events.js";
import { currentLang } from "../../../common/utils.js";

// CONSTANTS
const kWarningCriticalThreshold = 10;
const kLangToLocale = {
english: "en",
french: "fr"
};
const kOneDay = 1_000 * 60 * 60 * 24;
const kOneWeek = kOneDay * 7;
const kOneMonth = kOneDay * 30;
const kOneYear = kOneDay * 365;
const kModuleTypeColors = {
esm: "#10b981",
dual: "#06b6d4",
Expand All @@ -16,6 +25,25 @@ const kModuleTypeColors = {
faux: "#6b7280"
};

function formatTimeAgo(isoDate) {
const ageMs = Date.now() - new Date(isoDate).getTime();
const rtf = new Intl.RelativeTimeFormat(kLangToLocale[currentLang()] ?? "en", {
numeric: "auto"
});

if (ageMs < kOneWeek) {
return rtf.format(-Math.floor(ageMs / kOneDay), "day");
}
if (ageMs < kOneMonth) {
return rtf.format(-Math.floor(ageMs / kOneWeek), "week");
}
if (ageMs < kOneYear) {
return rtf.format(-Math.floor(ageMs / kOneMonth), "month");
}

return rtf.format(-Math.floor(ageMs / kOneYear), "year");
}

function renderFlag(flag) {
const ignoredFlags = window.settings.config.ignore.flags ?? [];
const ignoredSet = new Set(ignoredFlags);
Expand All @@ -35,7 +63,14 @@ function getVersionData(secureDataSet, name, version) {
return secureDataSet.data.dependencies[name]?.versions[version];
}

export function renderCardContent(secureDataSet, { nodeId, parentId = null, isRoot = false }) {
export function renderCardContent(secureDataSet, options) {
const {
nodeId,
parentId = null,
isRoot = false,
publishedAt = null,
publishedColor = null
} = options;
const entry = secureDataSet.linker.get(nodeId);
const versionData = getVersionData(secureDataSet, entry.name, entry.version);
if (!versionData) {
Expand Down Expand Up @@ -107,6 +142,17 @@ export function renderCardContent(secureDataSet, { nodeId, parentId = null, isRo
? nothing
: html`<div class="tree-card--stats"><span class="tree-card--separator">↳ ${parentName}</span></div>`
}
${publishedAt === null
? nothing
: html`
<div class="tree-card--published-row">
<span
class="tree-card--published-badge"
style="--published-color: ${publishedColor ?? "#6b7280"}"
>↻ ${formatTimeAgo(publishedAt)}</span>
</div>
`
}
</div>
`;
}
48 changes: 48 additions & 0 deletions public/components/views/tree/tree-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@ export const CARD_WIDTH = 250;
export const CONNECTOR_GAP = 16;
export const GAP_ROW_HEIGHT = 16;

const kEpochFallback = kEpochFallback;
const kOneDay = 1_000 * 60 * 60 * 24;
const kOneWeek = kOneDay * 7;
const kOneMonth = kOneDay * 30;
const kSixMonths = kOneDay * 30 * 6;
const kOneYear = kOneDay * 365;
const kTwoYears = kOneDay * 365 * 2;

export const ACTIVITY_GROUPS = [
{ key: "fresh", color: "#10b981", threshold: kOneWeek },
{ key: "recent", color: "#84cc16", threshold: kOneMonth },
{ key: "active", color: "#eab308", threshold: kSixMonths },
{ key: "stable", color: "#f97316", threshold: kOneYear },
{ key: "slow", color: "#ef4444", threshold: kTwoYears },
{ key: "stale", color: "#6b7280", threshold: Infinity }
];

export function getSortedChildren(nodeId, childrenByParent, linker) {
return (childrenByParent.get(nodeId) ?? [])
.sort((idA, idB) => linker.get(idA).name.localeCompare(linker.get(idB).name));
Expand All @@ -19,6 +36,37 @@ export function buildChildrenMap(rawEdgesData) {
return childrenByParent;
}

export function computeActivityGroups(linker, dependencies) {
const now = Date.now();
const groups = new Map(ACTIVITY_GROUPS.map(({ key }) => [key, []]));
const seen = new Set();

for (const [nodeId, entry] of linker) {
const spec = `${entry.name}@${entry.version}`;
if (seen.has(spec)) {
continue;
}
seen.add(spec);

const lastUpdateAt = dependencies[entry.name]?.metadata?.lastUpdateAt;
const ageMs = lastUpdateAt ? now - new Date(lastUpdateAt).getTime() : Infinity;

const bucket = ACTIVITY_GROUPS.find(({ threshold }) => ageMs < threshold) ?? ACTIVITY_GROUPS.at(-1);
groups.get(bucket.key).push(nodeId);
}

for (const [, nodeIds] of groups) {
nodeIds.sort((idA, idB) => {
const dateA = dependencies[linker.get(idA).name]?.metadata?.lastUpdateAt ?? kEpochFallback;
const dateB = dependencies[linker.get(idB).name]?.metadata?.lastUpdateAt ?? kEpochFallback;

return new Date(dateB).getTime() - new Date(dateA).getTime();
});
}

return groups;
}

export function computeDepthGroups(rawEdgesData) {
const childrenByParent = buildChildrenMap(rawEdgesData);

Expand Down
18 changes: 18 additions & 0 deletions public/components/views/tree/tree-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,24 @@ export const treeStyles = css`
margin-left: auto;
}

.tree-card--published-row {
display: flex;
margin-top: 2px;
}

.tree-card--published-badge {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 13px;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
background: color-mix(in srgb, var(--published-color) 15%, transparent);
border: 1px solid color-mix(in srgb, var(--published-color) 35%, transparent);
color: var(--published-color);
}

.depth-container {
display: flex;
flex-direction: row;
Expand Down
67 changes: 62 additions & 5 deletions public/components/views/tree/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ import { LitElement, html, nothing } from "lit";
import { currentLang } from "../../../common/utils.js";
import { EVENTS } from "../../../core/events.js";
import { treeStyles } from "./tree-styles.js";
import { CARD_WIDTH, CONNECTOR_GAP, GAP_ROW_HEIGHT, computeDepthGroups, computeTreeLayout } from "./tree-layout.js";
import {
CARD_WIDTH,
CONNECTOR_GAP,
GAP_ROW_HEIGHT,
ACTIVITY_GROUPS,
computeDepthGroups,
computeTreeLayout,
computeActivityGroups
} from "./tree-layout.js";
import { renderCardContent } from "./tree-card.js";
import { drawConnectors } from "./tree-connectors.js";
import "../../../components/root-selector/root-selector.js";
Expand Down Expand Up @@ -138,6 +146,41 @@ class TreeView extends LitElement {
`;
}

#renderActivityColumn(bucket, nodeIds) {
const i18n = window.i18n[currentLang()];
const labelKey = `activity${bucket.key.charAt(0).toUpperCase()}${bucket.key.slice(1)}`;

return html`
<div class="depth-column">
<div class="depth-column--header" style="border-bottom-color: ${bucket.color}">
<span class="depth-column--label" style="color: ${bucket.color}">${i18n.tree[labelKey]}</span>
<span class="depth-column--count" style="background: ${bucket.color}">${nodeIds.length}</span>
</div>
<div class="depth-column--cards">
${nodeIds.map((nodeId) => {
const entry = this.secureDataSet.linker.get(nodeId);
const publishedAt = this.secureDataSet.data.dependencies[entry.name]?.metadata?.lastUpdateAt ?? null;

return renderCardContent(this.secureDataSet, { nodeId, publishedAt, publishedColor: bucket.color });
})}
</div>
</div>
`;
}

#renderActivityMode() {
const activityGroups = computeActivityGroups(
this.secureDataSet.linker,
this.secureDataSet.data.dependencies
);

return html`
<div class="depth-container">
${ACTIVITY_GROUPS.map((bucket) => this.#renderActivityColumn(bucket, activityGroups.get(bucket.key)))}
</div>
`;
}

#renderHeader(depthGroups) {
const totalDeps = Object.keys(this.secureDataSet.data.dependencies).length;
const directDeps = (depthGroups.get(1) ?? []).length;
Expand Down Expand Up @@ -165,11 +208,28 @@ class TreeView extends LitElement {
this._mode = "tree";
}}
>${i18n.tree.modeTree}</button>
<button
class="mode-btn ${this._mode === "activity" ? "active" : ""}"
@click=${() => {
this._mode = "activity";
}}
>${i18n.tree.modeActivity}</button>
</div>
</div>
`;
}

#renderBody(depthGroups, maxDepth) {
if (this._mode === "tree") {
return this.#renderTreeMode(maxDepth);
}
else if (this._mode === "activity") {
return this.#renderActivityMode();
}

return this.#renderDepthMode(depthGroups);
}

render() {
if (!this.secureDataSet?.data) {
return nothing;
Expand All @@ -183,10 +243,7 @@ class TreeView extends LitElement {

return html`
${this.#renderHeader(depthGroups)}
${this._mode === "tree"
? this.#renderTreeMode(maxDepth)
: this.#renderDepthMode(depthGroups)
}
${this.#renderBody(depthGroups, maxDepth)}
`;
}
}
Expand Down
Loading