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
46 changes: 43 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"homepage": "https://github.com/jenkinsci/pipeline-graph-view-plugin/#readme",
"dependencies": {
"@formatjs/intl-durationformat": "^0.7.4",
"@tippyjs/react": "^4.2.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
Expand All @@ -56,6 +57,6 @@
"vitest": "^3.1.2"
},
"engines": {
"node": ">=22"
"node": ">=23"
}
}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<!-- Baseline Jenkins version you use to build the plugin. Users must have this version or newer to run. -->
<jenkins.baseline>2.479</jenkins.baseline>
<jenkins.version>${jenkins.baseline}.3</jenkins.version>
<node.version>22.15.0</node.version>
<node.version>24.2.0</node.version>
<npm.version>11.3.0</npm.version>
<spotless.check.skip>false</spotless.check.skip>
</properties>
Expand Down
4 changes: 2 additions & 2 deletions src/main/frontend/common/i18n/i18n-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
useState,
} from "react";

import { DEFAULT_LOCALE, LocaleContext } from "./locale-provider.tsx";
import { DEFAULT_LOCALE, useLocale } from "./locale-provider.tsx";
import {
defaultMessages,
getMessages,
Expand All @@ -29,7 +29,7 @@ export const I18NProvider: FunctionComponent<I18NProviderProps> = ({
children,
bundles,
}) => {
const locale = useContext(LocaleContext);
const locale = useLocale();

const [messages, setMessages] = useState<Messages>(defaultMessages(locale));

Expand Down
14 changes: 12 additions & 2 deletions src/main/frontend/common/i18n/locale-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Context, createContext, FunctionComponent, ReactNode } from "react";
import {
Context,
createContext,
FunctionComponent,
ReactNode,
useContext,
} from "react";

export const DEFAULT_LOCALE = "en";
export const LocaleContext: Context<string> = createContext(DEFAULT_LOCALE);
const LocaleContext: Context<string> = createContext(DEFAULT_LOCALE);

interface LocaleProviderProps {
children: ReactNode;
Expand All @@ -18,3 +24,7 @@ export const LocaleProvider: FunctionComponent<LocaleProviderProps> = ({
</LocaleContext.Provider>
);
};

export const useLocale = (): string => {
return useContext(LocaleContext);
};
12 changes: 3 additions & 9 deletions src/main/frontend/common/i18n/messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,9 @@ describe("Messages", () => {

const messages = await getMessages("en", [ResourceBundleName.messages]);

expect(messages.format(LocalizedMessageKey.second, { 0: 5 })).toEqual(
"5 sec",
);
expect(messages.format(LocalizedMessageKey.day, { 0: 1 })).toEqual(
"1 day",
);
expect(messages.format(LocalizedMessageKey.day, { 0: 2 })).toEqual(
"2 days",
);
expect(
messages.format(LocalizedMessageKey.startedAgo, { 0: "5s" }),
).toEqual("Started 5s ago");
expect(messages.format("A.property")).toEqual("");
});
});
Expand Down
16 changes: 2 additions & 14 deletions src/main/frontend/common/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,8 @@ export async function getMessages(
export type MessageKeyType = LocalizedMessageKey | string;

export enum LocalizedMessageKey {
millisecond = "timings.millisecond",
second = "timings.second",
minute = "timings.minute",
hour = "timings.hour",
day = "timings.day",
month = "timings.month",
year = "timings.year",
startedAgo = "startedAgo",
queued = "queued",
noBuilds = "noBuilds",
start = "node.start",
end = "node.end",
Expand All @@ -88,14 +82,8 @@ export enum LocalizedMessageKey {
}

const DEFAULT_MESSAGES: ResourceBundle = {
[LocalizedMessageKey.millisecond]: "{0} ms",
[LocalizedMessageKey.second]: "{0} sec",
[LocalizedMessageKey.minute]: "{0} min",
[LocalizedMessageKey.hour]: "{0} hr",
[LocalizedMessageKey.day]: "{0} {0,choice,0#days|1#day|1<days}",
[LocalizedMessageKey.month]: "{0} mo",
[LocalizedMessageKey.year]: "{0} yr",
[LocalizedMessageKey.startedAgo]: "Started {0} ago",
[LocalizedMessageKey.queued]: "Queued {0}",
[LocalizedMessageKey.noBuilds]: "No builds",
[LocalizedMessageKey.start]: "Start",
[LocalizedMessageKey.end]: "End",
Expand Down
77 changes: 26 additions & 51 deletions src/main/frontend/common/utils/timings.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,81 +3,56 @@
import { render } from "@testing-library/react";
import { vi } from "vitest";

import { I18NContext, LocalizedMessageKey, Messages } from "../i18n/index.ts";
import { Paused, Started, Total } from "./timings.tsx";

describe("Timings", () => {
const translations = new Messages(
{
[LocalizedMessageKey.year]: "{0} yr",
[LocalizedMessageKey.month]: "{0} mo",
[LocalizedMessageKey.day]: "{0} day",
[LocalizedMessageKey.hour]: "{0} hr",
[LocalizedMessageKey.minute]: "{0} min",
[LocalizedMessageKey.second]: "{0} sec",
[LocalizedMessageKey.millisecond]: "{0} ms",
[LocalizedMessageKey.startedAgo]: "Started {0} ago",
},
"en",
);

function process(child: any) {
return render(
<I18NContext.Provider value={translations}>{child}</I18NContext.Provider>,
);
}

describe("Total", () => {
function getTotal(ms: number) {
return process(<Total ms={ms} />);
return render(<Total ms={ms} />);
}

it("should format milliseconds to hours, minutes, and seconds", () => {
// First check 359 days.
expect(getTotal(31_017_600_000).getByText("11 mo")).toBeInTheDocument();
expect(getTotal(31_017_600_000).getByText("11 mths")).toBeInTheDocument();
// And 362 days.
expect(getTotal(31_276_800_000).getByText("12 mo")).toBeInTheDocument();
expect(getTotal(31_276_800_000).getByText("12 mths")).toBeInTheDocument();
// 11.25 years - Check that if the first unit has 2 or more digits, a second unit isn't used.
expect(getTotal(354_780_000_000).getByText("11 yr")).toBeInTheDocument();
expect(getTotal(354_780_000_000).getByText("11y")).toBeInTheDocument();
// 9.25 years - Check that if the first unit has only 1 digit, a second unit is used.
expect(
getTotal(291_708_000_000).getByText("9 yr 3 mo"),
).toBeInTheDocument();
expect(getTotal(291_708_000_000).getByText("9y 3m")).toBeInTheDocument();
// 3 months 14 days
expect(
getTotal(8_985_600_000).getByText("3 mo 14 day"),
).toBeInTheDocument();
expect(getTotal(8_985_600_000).getByText("3m 14d")).toBeInTheDocument();
// 2 day 4 hours
expect(getTotal(187_200_000).getByText("2 day 4 hr")).toBeInTheDocument();
expect(getTotal(187_200_000).getByText("2d 4h")).toBeInTheDocument();
// 8 hours 46 minutes
expect(getTotal(31_560_000).getByText("8 hr 46 min")).toBeInTheDocument();
expect(getTotal(31_560_000).getByText("8h 46m")).toBeInTheDocument();
// 67 seconds -> 1 minute 7 seconds
expect(getTotal(67_000).getByText("1 min 7 sec")).toBeInTheDocument();
expect(getTotal(67_000).getByText("1m 7s")).toBeInTheDocument();
// 17 seconds - Check that times less than a minute only use seconds.
expect(getTotal(17_000).getByText("17 sec")).toBeInTheDocument();
expect(getTotal(17_000).getByText("17s")).toBeInTheDocument();
// 1712ms -> 1.7sec
expect(getTotal(1_712).getByText("1.7 sec")).toBeInTheDocument();
expect(getTotal(1_712).getByText("1.7s")).toBeInTheDocument();
// 171ms -> 0.17sec
expect(getTotal(171).getByText("0.17 sec")).toBeInTheDocument();
// 101ms -> 0.10sec
expect(getTotal(101).getByText("0.1 sec")).toBeInTheDocument();
expect(getTotal(171).getByText("0.17s")).toBeInTheDocument();
// 101ms -> 0.1sec
expect(getTotal(101).getByText("0.1s")).toBeInTheDocument();
// 17ms
expect(getTotal(17).getByText("17 ms")).toBeInTheDocument();
expect(getTotal(17).getByText("17ms")).toBeInTheDocument();
// 1ms
expect(getTotal(1).getByText("1 ms")).toBeInTheDocument();
expect(getTotal(1).getByText("1ms")).toBeInTheDocument();
});
});

describe("paused", () => {
function getPaused(since: number) {
return process(<Paused since={since} />);
return render(<Paused since={since} />);
}

it("should prefix the time with Queued", () => {
expect(getPaused(1000).getByText("Queued 1 sec")).toBeInTheDocument();
expect(getPaused(100).getByText("Queued 0.1 sec")).toBeInTheDocument();
expect(getPaused(10).getByText("Queued 10 ms")).toBeInTheDocument();
expect(getPaused(1).getByText("Queued 1 ms")).toBeInTheDocument();
expect(getPaused(1000).getByText("Queued 1s")).toBeInTheDocument();
expect(getPaused(100).getByText("Queued 0.1s")).toBeInTheDocument();
expect(getPaused(10).getByText("Queued 10ms")).toBeInTheDocument();
expect(getPaused(1).getByText("Queued 1ms")).toBeInTheDocument();
});
});

Expand All @@ -93,7 +68,7 @@ describe("Timings", () => {
});

function getStarted(since: number) {
return process(<Started since={since} />);
return render(<Started since={since} />);
}

it("should return empty element if since is 0", () => {
Expand All @@ -102,16 +77,16 @@ describe("Timings", () => {

it("should prefix the time with Started and end with ago", () => {
expect(
getStarted(now - 1000).getByText("Started 1 sec ago"),
getStarted(now - 1000).getByText("Started 1s ago"),
).toBeInTheDocument();
expect(
getStarted(now - 100).getByText("Started 0.1 sec ago"),
getStarted(now - 100).getByText("Started 0.1s ago"),
).toBeInTheDocument();
expect(
getStarted(now - 10).getByText("Started 10 ms ago"),
getStarted(now - 10).getByText("Started 10ms ago"),
).toBeInTheDocument();
expect(
getStarted(now - 1).getByText("Started 1 ms ago"),
getStarted(now - 1).getByText("Started 1ms ago"),
).toBeInTheDocument();
});
});
Expand Down
Loading