Skip to content

Commit 9051e54

Browse files
feat(interface): introduce new activity tab on tree page (#713)
1 parent 696c062 commit 9051e54

6 files changed

Lines changed: 191 additions & 8 deletions

File tree

i18n/english.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,14 @@ const ui = {
245245
deps: "deps",
246246
direct: "direct",
247247
modeDepth: "Depth",
248-
modeTree: "Tree"
248+
modeTree: "Tree",
249+
modeActivity: "Activity",
250+
activityFresh: "< 1 week",
251+
activityRecent: "< 1 month",
252+
activityActive: "< 6 months",
253+
activityStable: "< 1 year",
254+
activitySlow: "< 2 years",
255+
activityStale: "Stale"
249256
},
250257
search_command: {
251258
placeholder: "Search packages...",

i18n/french.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,14 @@ const ui = {
245245
deps: "dépendances",
246246
direct: "directes",
247247
modeDepth: "Profondeur",
248-
modeTree: "Arbre"
248+
modeTree: "Arbre",
249+
modeActivity: "Activité",
250+
activityFresh: "< 1 semaine",
251+
activityRecent: "< 1 mois",
252+
activityActive: "< 6 mois",
253+
activityStable: "< 1 an",
254+
activitySlow: "< 2 ans",
255+
activityStale: "Abandonné"
249256
},
250257
search_command: {
251258
placeholder: "Rechercher des packages...",

public/components/views/tree/tree-card.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,18 @@ import prettyBytes from "pretty-bytes";
55

66
// Import Internal Dependencies
77
import { EVENTS } from "../../../core/events.js";
8+
import { currentLang } from "../../../common/utils.js";
89

910
// CONSTANTS
1011
const kWarningCriticalThreshold = 10;
12+
const kLangToLocale = {
13+
english: "en",
14+
french: "fr"
15+
};
16+
const kOneDay = 1_000 * 60 * 60 * 24;
17+
const kOneWeek = kOneDay * 7;
18+
const kOneMonth = kOneDay * 30;
19+
const kOneYear = kOneDay * 365;
1120
const kModuleTypeColors = {
1221
esm: "#10b981",
1322
dual: "#06b6d4",
@@ -16,6 +25,25 @@ const kModuleTypeColors = {
1625
faux: "#6b7280"
1726
};
1827

28+
function formatTimeAgo(isoDate) {
29+
const ageMs = Date.now() - new Date(isoDate).getTime();
30+
const rtf = new Intl.RelativeTimeFormat(kLangToLocale[currentLang()] ?? "en", {
31+
numeric: "auto"
32+
});
33+
34+
if (ageMs < kOneWeek) {
35+
return rtf.format(-Math.floor(ageMs / kOneDay), "day");
36+
}
37+
if (ageMs < kOneMonth) {
38+
return rtf.format(-Math.floor(ageMs / kOneWeek), "week");
39+
}
40+
if (ageMs < kOneYear) {
41+
return rtf.format(-Math.floor(ageMs / kOneMonth), "month");
42+
}
43+
44+
return rtf.format(-Math.floor(ageMs / kOneYear), "year");
45+
}
46+
1947
function renderFlag(flag) {
2048
const ignoredFlags = window.settings.config.ignore.flags ?? [];
2149
const ignoredSet = new Set(ignoredFlags);
@@ -35,7 +63,14 @@ function getVersionData(secureDataSet, name, version) {
3563
return secureDataSet.data.dependencies[name]?.versions[version];
3664
}
3765

38-
export function renderCardContent(secureDataSet, { nodeId, parentId = null, isRoot = false }) {
66+
export function renderCardContent(secureDataSet, options) {
67+
const {
68+
nodeId,
69+
parentId = null,
70+
isRoot = false,
71+
publishedAt = null,
72+
publishedColor = null
73+
} = options;
3974
const entry = secureDataSet.linker.get(nodeId);
4075
const versionData = getVersionData(secureDataSet, entry.name, entry.version);
4176
if (!versionData) {
@@ -107,6 +142,17 @@ export function renderCardContent(secureDataSet, { nodeId, parentId = null, isRo
107142
? nothing
108143
: html`<div class="tree-card--stats"><span class="tree-card--separator">${parentName}</span></div>`
109144
}
145+
${publishedAt === null
146+
? nothing
147+
: html`
148+
<div class="tree-card--published-row">
149+
<span
150+
class="tree-card--published-badge"
151+
style="--published-color: ${publishedColor ?? "#6b7280"}"
152+
>${formatTimeAgo(publishedAt)}</span>
153+
</div>
154+
`
155+
}
110156
</div>
111157
`;
112158
}

public/components/views/tree/tree-layout.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@ export const CARD_WIDTH = 250;
33
export const CONNECTOR_GAP = 16;
44
export const GAP_ROW_HEIGHT = 16;
55

6+
const kEpochFallback = kEpochFallback;
7+
const kOneDay = 1_000 * 60 * 60 * 24;
8+
const kOneWeek = kOneDay * 7;
9+
const kOneMonth = kOneDay * 30;
10+
const kSixMonths = kOneDay * 30 * 6;
11+
const kOneYear = kOneDay * 365;
12+
const kTwoYears = kOneDay * 365 * 2;
13+
14+
export const ACTIVITY_GROUPS = [
15+
{ key: "fresh", color: "#10b981", threshold: kOneWeek },
16+
{ key: "recent", color: "#84cc16", threshold: kOneMonth },
17+
{ key: "active", color: "#eab308", threshold: kSixMonths },
18+
{ key: "stable", color: "#f97316", threshold: kOneYear },
19+
{ key: "slow", color: "#ef4444", threshold: kTwoYears },
20+
{ key: "stale", color: "#6b7280", threshold: Infinity }
21+
];
22+
623
export function getSortedChildren(nodeId, childrenByParent, linker) {
724
return (childrenByParent.get(nodeId) ?? [])
825
.sort((idA, idB) => linker.get(idA).name.localeCompare(linker.get(idB).name));
@@ -19,6 +36,37 @@ export function buildChildrenMap(rawEdgesData) {
1936
return childrenByParent;
2037
}
2138

39+
export function computeActivityGroups(linker, dependencies) {
40+
const now = Date.now();
41+
const groups = new Map(ACTIVITY_GROUPS.map(({ key }) => [key, []]));
42+
const seen = new Set();
43+
44+
for (const [nodeId, entry] of linker) {
45+
const spec = `${entry.name}@${entry.version}`;
46+
if (seen.has(spec)) {
47+
continue;
48+
}
49+
seen.add(spec);
50+
51+
const lastUpdateAt = dependencies[entry.name]?.metadata?.lastUpdateAt;
52+
const ageMs = lastUpdateAt ? now - new Date(lastUpdateAt).getTime() : Infinity;
53+
54+
const bucket = ACTIVITY_GROUPS.find(({ threshold }) => ageMs < threshold) ?? ACTIVITY_GROUPS.at(-1);
55+
groups.get(bucket.key).push(nodeId);
56+
}
57+
58+
for (const [, nodeIds] of groups) {
59+
nodeIds.sort((idA, idB) => {
60+
const dateA = dependencies[linker.get(idA).name]?.metadata?.lastUpdateAt ?? kEpochFallback;
61+
const dateB = dependencies[linker.get(idB).name]?.metadata?.lastUpdateAt ?? kEpochFallback;
62+
63+
return new Date(dateB).getTime() - new Date(dateA).getTime();
64+
});
65+
}
66+
67+
return groups;
68+
}
69+
2270
export function computeDepthGroups(rawEdgesData) {
2371
const childrenByParent = buildChildrenMap(rawEdgesData);
2472

public/components/views/tree/tree-styles.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,24 @@ export const treeStyles = css`
297297
margin-left: auto;
298298
}
299299
300+
.tree-card--published-row {
301+
display: flex;
302+
margin-top: 2px;
303+
}
304+
305+
.tree-card--published-badge {
306+
display: inline-flex;
307+
align-items: center;
308+
gap: 3px;
309+
font-size: 13px;
310+
font-weight: 600;
311+
padding: 2px 8px;
312+
border-radius: 10px;
313+
background: color-mix(in srgb, var(--published-color) 15%, transparent);
314+
border: 1px solid color-mix(in srgb, var(--published-color) 35%, transparent);
315+
color: var(--published-color);
316+
}
317+
300318
.depth-container {
301319
display: flex;
302320
flex-direction: row;

public/components/views/tree/tree.js

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ import { LitElement, html, nothing } from "lit";
55
import { currentLang } from "../../../common/utils.js";
66
import { EVENTS } from "../../../core/events.js";
77
import { treeStyles } from "./tree-styles.js";
8-
import { CARD_WIDTH, CONNECTOR_GAP, GAP_ROW_HEIGHT, computeDepthGroups, computeTreeLayout } from "./tree-layout.js";
8+
import {
9+
CARD_WIDTH,
10+
CONNECTOR_GAP,
11+
GAP_ROW_HEIGHT,
12+
ACTIVITY_GROUPS,
13+
computeDepthGroups,
14+
computeTreeLayout,
15+
computeActivityGroups
16+
} from "./tree-layout.js";
917
import { renderCardContent } from "./tree-card.js";
1018
import { drawConnectors } from "./tree-connectors.js";
1119
import "../../../components/root-selector/root-selector.js";
@@ -138,6 +146,41 @@ class TreeView extends LitElement {
138146
`;
139147
}
140148

149+
#renderActivityColumn(bucket, nodeIds) {
150+
const i18n = window.i18n[currentLang()];
151+
const labelKey = `activity${bucket.key.charAt(0).toUpperCase()}${bucket.key.slice(1)}`;
152+
153+
return html`
154+
<div class="depth-column">
155+
<div class="depth-column--header" style="border-bottom-color: ${bucket.color}">
156+
<span class="depth-column--label" style="color: ${bucket.color}">${i18n.tree[labelKey]}</span>
157+
<span class="depth-column--count" style="background: ${bucket.color}">${nodeIds.length}</span>
158+
</div>
159+
<div class="depth-column--cards">
160+
${nodeIds.map((nodeId) => {
161+
const entry = this.secureDataSet.linker.get(nodeId);
162+
const publishedAt = this.secureDataSet.data.dependencies[entry.name]?.metadata?.lastUpdateAt ?? null;
163+
164+
return renderCardContent(this.secureDataSet, { nodeId, publishedAt, publishedColor: bucket.color });
165+
})}
166+
</div>
167+
</div>
168+
`;
169+
}
170+
171+
#renderActivityMode() {
172+
const activityGroups = computeActivityGroups(
173+
this.secureDataSet.linker,
174+
this.secureDataSet.data.dependencies
175+
);
176+
177+
return html`
178+
<div class="depth-container">
179+
${ACTIVITY_GROUPS.map((bucket) => this.#renderActivityColumn(bucket, activityGroups.get(bucket.key)))}
180+
</div>
181+
`;
182+
}
183+
141184
#renderHeader(depthGroups) {
142185
const totalDeps = Object.keys(this.secureDataSet.data.dependencies).length;
143186
const directDeps = (depthGroups.get(1) ?? []).length;
@@ -165,11 +208,28 @@ class TreeView extends LitElement {
165208
this._mode = "tree";
166209
}}
167210
>${i18n.tree.modeTree}</button>
211+
<button
212+
class="mode-btn ${this._mode === "activity" ? "active" : ""}"
213+
@click=${() => {
214+
this._mode = "activity";
215+
}}
216+
>${i18n.tree.modeActivity}</button>
168217
</div>
169218
</div>
170219
`;
171220
}
172221

222+
#renderBody(depthGroups, maxDepth) {
223+
if (this._mode === "tree") {
224+
return this.#renderTreeMode(maxDepth);
225+
}
226+
else if (this._mode === "activity") {
227+
return this.#renderActivityMode();
228+
}
229+
230+
return this.#renderDepthMode(depthGroups);
231+
}
232+
173233
render() {
174234
if (!this.secureDataSet?.data) {
175235
return nothing;
@@ -183,10 +243,7 @@ class TreeView extends LitElement {
183243

184244
return html`
185245
${this.#renderHeader(depthGroups)}
186-
${this._mode === "tree"
187-
? this.#renderTreeMode(maxDepth)
188-
: this.#renderDepthMode(depthGroups)
189-
}
246+
${this.#renderBody(depthGroups, maxDepth)}
190247
`;
191248
}
192249
}

0 commit comments

Comments
 (0)