mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
chore: replace date-fns by dayjs (#18022)
This change replaces date-fns with dayjs throughout the codebase for more consistent date/time handling and to reduce bundle size. It also tries to make the formatting and usage consistent. **Why dayjs over date-fns?** Just because we were using dayjs more broadly. Its formatting capabilities, were also easier to extend.
This commit is contained in:
+13
-18
@@ -2,7 +2,13 @@ import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { API, type DeploymentConfig } from "api/api";
|
||||
import type { SerpentOption } from "api/typesGenerated";
|
||||
import { formatDuration, intervalToDuration } from "date-fns";
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
import { humanDuration } from "utils/time";
|
||||
import { coderPort, defaultPassword } from "./constants";
|
||||
import { type LoginOptions, findSessionToken, randomName } from "./helpers";
|
||||
|
||||
@@ -237,13 +243,6 @@ export async function verifyConfigFlagString(
|
||||
await expect(configOption).toHaveText(opt.value as any);
|
||||
}
|
||||
|
||||
export async function verifyConfigFlagEmpty(page: Page, flag: string) {
|
||||
const configOption = page.locator(
|
||||
`div.options-table .option-${flag} .option-value-empty`,
|
||||
);
|
||||
await expect(configOption).toHaveText("Not set");
|
||||
}
|
||||
|
||||
export async function verifyConfigFlagArray(
|
||||
page: Page,
|
||||
config: DeploymentConfig,
|
||||
@@ -290,19 +289,15 @@ export async function verifyConfigFlagDuration(
|
||||
flag: string,
|
||||
) {
|
||||
const opt = findConfigOption(config, flag);
|
||||
if (typeof opt.value !== "number") {
|
||||
throw new Error(
|
||||
`Option with env ${flag} should be a number, but got ${typeof opt.value}.`,
|
||||
);
|
||||
}
|
||||
const configOption = page.locator(
|
||||
`div.options-table .option-${flag} .option-value-string`,
|
||||
);
|
||||
//
|
||||
await expect(configOption).toHaveText(
|
||||
formatDuration(
|
||||
// intervalToDuration takes ms, so convert nanoseconds to ms
|
||||
intervalToDuration({
|
||||
start: 0,
|
||||
end: (opt.value as number) / 1e6,
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expect(configOption).toHaveText(humanDuration(opt.value / 1e6));
|
||||
}
|
||||
|
||||
export function findConfigOption(
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
verifyConfigFlagArray,
|
||||
verifyConfigFlagBoolean,
|
||||
verifyConfigFlagDuration,
|
||||
verifyConfigFlagEmpty,
|
||||
verifyConfigFlagString,
|
||||
} from "../../api";
|
||||
import { login } from "../../helpers";
|
||||
@@ -28,7 +27,11 @@ test("enabled observability settings", async ({ page }) => {
|
||||
await verifyConfigFlagBoolean(page, config, "enable-terraform-debug-mode");
|
||||
await verifyConfigFlagBoolean(page, config, "enable-terraform-debug-mode");
|
||||
await verifyConfigFlagDuration(page, config, "health-check-refresh");
|
||||
await verifyConfigFlagEmpty(page, "health-check-threshold-database");
|
||||
await verifyConfigFlagDuration(
|
||||
page,
|
||||
config,
|
||||
"health-check-threshold-database",
|
||||
);
|
||||
await verifyConfigFlagString(page, config, "log-human");
|
||||
await verifyConfigFlagString(page, config, "prometheus-address");
|
||||
await verifyConfigFlagArray(
|
||||
|
||||
+2
-1
@@ -85,12 +85,12 @@
|
||||
"color-convert": "2.0.1",
|
||||
"cron-parser": "4.9.0",
|
||||
"cronstrue": "2.50.0",
|
||||
"date-fns": "2.30.0",
|
||||
"dayjs": "1.11.13",
|
||||
"emoji-mart": "5.6.0",
|
||||
"file-saver": "2.0.5",
|
||||
"formik": "2.4.6",
|
||||
"front-matter": "4.0.2",
|
||||
"humanize-duration": "3.32.2",
|
||||
"jszip": "3.10.1",
|
||||
"lodash": "4.17.21",
|
||||
"lucide-react": "0.474.0",
|
||||
@@ -149,6 +149,7 @@
|
||||
"@types/color-convert": "2.0.4",
|
||||
"@types/express": "4.17.17",
|
||||
"@types/file-saver": "2.0.7",
|
||||
"@types/humanize-duration": "3.27.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.15",
|
||||
"@types/node": "20.17.16",
|
||||
|
||||
Generated
+16
-3
@@ -166,9 +166,6 @@ importers:
|
||||
cronstrue:
|
||||
specifier: 2.50.0
|
||||
version: 2.50.0
|
||||
date-fns:
|
||||
specifier: 2.30.0
|
||||
version: 2.30.0
|
||||
dayjs:
|
||||
specifier: 1.11.13
|
||||
version: 1.11.13
|
||||
@@ -184,6 +181,9 @@ importers:
|
||||
front-matter:
|
||||
specifier: 4.0.2
|
||||
version: 4.0.2
|
||||
humanize-duration:
|
||||
specifier: 3.32.2
|
||||
version: 3.32.2
|
||||
jszip:
|
||||
specifier: 3.10.1
|
||||
version: 3.10.1
|
||||
@@ -353,6 +353,9 @@ importers:
|
||||
'@types/file-saver':
|
||||
specifier: 2.0.7
|
||||
version: 2.0.7
|
||||
'@types/humanize-duration':
|
||||
specifier: 3.27.4
|
||||
version: 3.27.4
|
||||
'@types/jest':
|
||||
specifier: 29.5.14
|
||||
version: 29.5.14
|
||||
@@ -2574,6 +2577,9 @@ packages:
|
||||
'@types/http-errors@2.0.1':
|
||||
resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==, tarball: https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz}
|
||||
|
||||
'@types/humanize-duration@3.27.4':
|
||||
resolution: {integrity: sha512-yaf7kan2Sq0goxpbcwTQ+8E9RP6HutFBPv74T/IA/ojcHKhuKVlk2YFYyHhWZeLvZPzzLE3aatuQB4h0iqyyUA==, tarball: https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.27.4.tgz}
|
||||
|
||||
'@types/istanbul-lib-coverage@2.0.5':
|
||||
resolution: {integrity: sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==, tarball: https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz}
|
||||
|
||||
@@ -3992,6 +3998,9 @@ packages:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==, tarball: https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz}
|
||||
engines: {node: '>=10.17.0'}
|
||||
|
||||
humanize-duration@3.32.2:
|
||||
resolution: {integrity: sha512-jcTwWYeCJf4dN5GJnjBmHd42bNyK94lY49QTkrsAQrMTUoIYLevvDpmQtg5uv8ZrdIRIbzdasmSNZ278HHUPEg==, tarball: https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.32.2.tgz}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==, tarball: https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -8731,6 +8740,8 @@ snapshots:
|
||||
|
||||
'@types/http-errors@2.0.1': {}
|
||||
|
||||
'@types/humanize-duration@3.27.4': {}
|
||||
|
||||
'@types/istanbul-lib-coverage@2.0.5': {}
|
||||
|
||||
'@types/istanbul-lib-coverage@2.0.6': {}
|
||||
@@ -10330,6 +10341,8 @@ snapshots:
|
||||
|
||||
human-signals@2.1.0: {}
|
||||
|
||||
humanize-duration@3.32.2: {}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import type { FC, HTMLAttributes } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
import { isAfter, relativeTime, subtractTime } from "utils/time";
|
||||
|
||||
interface LastSeenProps
|
||||
extends Omit<HTMLAttributes<HTMLSpanElement>, "children"> {
|
||||
@@ -15,21 +13,25 @@ interface LastSeenProps
|
||||
export const LastSeen: FC<LastSeenProps> = ({ at, className, ...attrs }) => {
|
||||
const theme = useTheme();
|
||||
const t = dayjs(at);
|
||||
const now = dayjs();
|
||||
const now = new Date();
|
||||
const oneHourAgo = subtractTime(now, 1, "hour");
|
||||
const threeDaysAgo = subtractTime(now, 3, "day");
|
||||
const oneMonthAgo = subtractTime(now, 1, "month");
|
||||
const centuryAgo = subtractTime(now, 100, "year");
|
||||
|
||||
let message = t.fromNow();
|
||||
let message = relativeTime(at);
|
||||
let color = theme.palette.text.secondary;
|
||||
|
||||
if (t.isAfter(now.subtract(1, "hour"))) {
|
||||
if (isAfter(at, oneHourAgo)) {
|
||||
// Since the agent reports on a 10m interval,
|
||||
// the last_used_at can be inaccurate when recent.
|
||||
message = "Now";
|
||||
color = theme.roles.success.fill.solid;
|
||||
} else if (t.isAfter(now.subtract(3, "day"))) {
|
||||
} else if (isAfter(at, threeDaysAgo)) {
|
||||
color = theme.experimental.l2.text;
|
||||
} else if (t.isAfter(now.subtract(1, "month"))) {
|
||||
} else if (isAfter(at, oneMonthAgo)) {
|
||||
color = theme.roles.warning.fill.solid;
|
||||
} else if (t.isAfter(now.subtract(100, "year"))) {
|
||||
} else if (isAfter(at, centuryAgo)) {
|
||||
color = theme.roles.error.fill.solid;
|
||||
} else {
|
||||
message = "Never";
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import formatRelative from "date-fns/formatRelative";
|
||||
import subDays from "date-fns/subDays";
|
||||
import dayjs from "dayjs";
|
||||
import calendar from "dayjs/plugin/calendar";
|
||||
|
||||
dayjs.extend(calendar);
|
||||
|
||||
export const createDisplayDate = (
|
||||
date: Date,
|
||||
base: Date = new Date(),
|
||||
): string => {
|
||||
const lastWeek = subDays(base, 7);
|
||||
const lastWeek = dayjs(base).subtract(7, "day").toDate();
|
||||
if (date >= lastWeek) {
|
||||
return formatRelative(date, base).split(" at ")[0];
|
||||
return dayjs(date).calendar(dayjs(base), {
|
||||
sameDay: "[Today]",
|
||||
lastDay: "[Yesterday]",
|
||||
lastWeek: "[last] dddd",
|
||||
sameElse: "MM/DD/YYYY",
|
||||
});
|
||||
}
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import type { Workspace } from "api/typesGenerated";
|
||||
import { Badge } from "components/Badge/Badge";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import type { FC } from "react";
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
formatDateTime,
|
||||
relativeTimeWithoutSuffix,
|
||||
} from "utils/time";
|
||||
|
||||
export type WorkspaceDormantBadgeProps = {
|
||||
workspace: Workspace;
|
||||
@@ -11,25 +15,14 @@ export type WorkspaceDormantBadgeProps = {
|
||||
export const WorkspaceDormantBadge: FC<WorkspaceDormantBadgeProps> = ({
|
||||
workspace,
|
||||
}) => {
|
||||
const formatDate = (dateStr: string): string => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString(undefined, {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
return workspace.deleting_at ? (
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
This workspace has not been used for{" "}
|
||||
{formatDistanceToNow(Date.parse(workspace.last_used_at))} and has been
|
||||
{relativeTimeWithoutSuffix(workspace.last_used_at)} and has been
|
||||
marked dormant. It is scheduled to be deleted on{" "}
|
||||
{formatDate(workspace.deleting_at)}.
|
||||
{formatDateTime(workspace.deleting_at, DATE_FORMAT.FULL_DATETIME)}.
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -42,7 +35,7 @@ export const WorkspaceDormantBadge: FC<WorkspaceDormantBadgeProps> = ({
|
||||
title={
|
||||
<>
|
||||
This workspace has not been used for{" "}
|
||||
{formatDistanceToNow(Date.parse(workspace.last_used_at))} and has been
|
||||
{relativeTimeWithoutSuffix(workspace.last_used_at)} and has been
|
||||
marked dormant. It is not scheduled for auto-deletion but will become
|
||||
a candidate if auto-deletion is enabled on this template.
|
||||
</>
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { GetLicensesResponse } from "api/api";
|
||||
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { compareAsc } from "date-fns";
|
||||
import dayjs from "dayjs";
|
||||
import { type FC, useState } from "react";
|
||||
|
||||
@@ -92,10 +91,7 @@ export const LicenseCard: FC<LicenseCardProps> = ({
|
||||
alignItems="center"
|
||||
width="134px" // standardize width of date column
|
||||
>
|
||||
{compareAsc(
|
||||
new Date(license.claims.license_expires * 1000),
|
||||
new Date(),
|
||||
) < 1 ? (
|
||||
{dayjs(license.claims.license_expires * 1000).isBefore(dayjs()) ? (
|
||||
<Pill css={styles.expiredBadge} type="error">
|
||||
Expired
|
||||
</Pill>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SerpentOption } from "api/typesGenerated";
|
||||
import { formatDuration, intervalToDuration } from "date-fns";
|
||||
import { humanDuration } from "utils/time";
|
||||
|
||||
// optionValue is a helper function to format the value of a specific deployment options
|
||||
export function optionValue(
|
||||
@@ -14,13 +14,7 @@ export function optionValue(
|
||||
}
|
||||
switch (k) {
|
||||
case "format_duration":
|
||||
return formatDuration(
|
||||
// intervalToDuration takes ms, so convert nanoseconds to ms
|
||||
intervalToDuration({
|
||||
start: 0,
|
||||
end: (option.value as number) / 1e6,
|
||||
}),
|
||||
);
|
||||
return humanDuration((option.value as number) / 1e6);
|
||||
// Add additional cases here as needed.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/deprecated/Popover/Popover";
|
||||
import {
|
||||
addDays,
|
||||
addHours,
|
||||
format,
|
||||
isToday,
|
||||
startOfDay,
|
||||
startOfHour,
|
||||
subDays,
|
||||
} from "date-fns";
|
||||
import dayjs from "dayjs";
|
||||
import { MoveRightIcon } from "lucide-react";
|
||||
import { type ComponentProps, type FC, useRef, useState } from "react";
|
||||
import { DateRangePicker, createStaticRanges } from "react-date-range";
|
||||
@@ -55,9 +47,9 @@ export const DateRange: FC<DateRangeProps> = ({ value, onChange }) => {
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger>
|
||||
<Button variant="outline">
|
||||
<span>{format(value.startDate, "MMM d, Y")}</span>
|
||||
<span>{dayjs(value.startDate).format("MMM D, YYYY")}</span>
|
||||
<MoveRightIcon />
|
||||
<span>{format(value.endDate, "MMM d, Y")}</span>
|
||||
<span>{dayjs(value.endDate).format("MMM D, YYYY")}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
@@ -79,10 +71,10 @@ export const DateRange: FC<DateRangeProps> = ({ value, onChange }) => {
|
||||
const endDate = range.endDate as Date;
|
||||
const now = new Date();
|
||||
onChange({
|
||||
startDate: startOfDay(startDate),
|
||||
endDate: isToday(endDate)
|
||||
? startOfHour(addHours(now, 1))
|
||||
: startOfDay(addDays(endDate, 1)),
|
||||
startDate: dayjs(startDate).startOf("day").toDate(),
|
||||
endDate: dayjs(endDate).isSame(dayjs(), "day")
|
||||
? dayjs(now).startOf("hour").add(1, "hour").toDate()
|
||||
: dayjs(endDate).startOf("day").add(1, "day").toDate(),
|
||||
});
|
||||
setOpen(false);
|
||||
}}
|
||||
@@ -102,28 +94,28 @@ export const DateRange: FC<DateRangeProps> = ({ value, onChange }) => {
|
||||
{
|
||||
label: "Yesterday",
|
||||
range: () => ({
|
||||
startDate: subDays(new Date(), 1),
|
||||
endDate: subDays(new Date(), 1),
|
||||
startDate: dayjs().subtract(1, "day").toDate(),
|
||||
endDate: dayjs().subtract(1, "day").toDate(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
range: () => ({
|
||||
startDate: subDays(new Date(), 6),
|
||||
startDate: dayjs().subtract(6, "day").toDate(),
|
||||
endDate: new Date(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Last 14 days",
|
||||
range: () => ({
|
||||
startDate: subDays(new Date(), 13),
|
||||
startDate: dayjs().subtract(13, "day").toDate(),
|
||||
endDate: new Date(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
range: () => ({
|
||||
startDate: subDays(new Date(), 29),
|
||||
startDate: dayjs().subtract(29, "day").toDate(),
|
||||
endDate: new Date(),
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -35,14 +35,6 @@ import {
|
||||
} from "components/HelpTooltip/HelpTooltip";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import {
|
||||
addHours,
|
||||
addWeeks,
|
||||
format,
|
||||
startOfDay,
|
||||
startOfHour,
|
||||
subDays,
|
||||
} from "date-fns";
|
||||
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
|
||||
import { CircleCheck as CircleCheckIcon } from "lucide-react";
|
||||
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
|
||||
@@ -57,6 +49,13 @@ import { Helmet } from "react-helmet-async";
|
||||
import { useQuery } from "react-query";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { getLatencyColor } from "utils/latency";
|
||||
import {
|
||||
addTime,
|
||||
formatDateTime,
|
||||
startOfDay,
|
||||
startOfHour,
|
||||
subtractTime,
|
||||
} from "utils/time";
|
||||
import { getTemplatePageTitle } from "../utils";
|
||||
import { DateRange as DailyPicker, type DateRangeValue } from "./DateRange";
|
||||
import { type InsightsInterval, IntervalMenu } from "./IntervalMenu";
|
||||
@@ -138,7 +137,7 @@ export default function TemplateInsightsPage() {
|
||||
const getDefaultInterval = (template: Template) => {
|
||||
const now = new Date();
|
||||
const templateCreateDate = new Date(template.created_at);
|
||||
const hasFiveWeeksOrMore = addWeeks(templateCreateDate, 5) < now;
|
||||
const hasFiveWeeksOrMore = addTime(templateCreateDate, 5, "week") < now;
|
||||
return hasFiveWeeksOrMore ? "week" : "day";
|
||||
};
|
||||
|
||||
@@ -162,9 +161,9 @@ const getDateRange = (
|
||||
// instantiation.
|
||||
const today = new Date();
|
||||
return {
|
||||
startDate: startOfDay(subDays(today, 6)),
|
||||
startDate: startOfDay(subtractTime(today, 6, "day")),
|
||||
// Add one hour to endDate to include real-time data for today.
|
||||
endDate: addHours(startOfHour(today), 1),
|
||||
endDate: addTime(startOfHour(today), 1, "hour"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -910,7 +909,7 @@ function formatTime(seconds: number): string {
|
||||
}
|
||||
|
||||
function toISOLocal(d: Date, offset: number) {
|
||||
return format(d, `yyyy-MM-dd'T'HH:mm:ss${formatOffset(offset)}`);
|
||||
return formatDateTime(d, `YYYY-MM-DD[T]HH:mm:ss${formatOffset(offset)}`);
|
||||
}
|
||||
|
||||
function formatOffset(offset: number): string {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Button from "@mui/material/Button";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { differenceInWeeks } from "date-fns";
|
||||
import dayjs from "dayjs";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { type FC, useRef, useState } from "react";
|
||||
@@ -20,7 +20,10 @@ interface WeekPickerProps {
|
||||
export const WeekPicker: FC<WeekPickerProps> = ({ value, onChange }) => {
|
||||
const anchorRef = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const numberOfWeeks = differenceInWeeks(value.endDate, value.startDate);
|
||||
const numberOfWeeks = dayjs(value.endDate).diff(
|
||||
dayjs(value.startDate),
|
||||
"week",
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { startOfDay, subDays } from "date-fns";
|
||||
import { startOfDay, subtractTime } from "utils/time";
|
||||
|
||||
export const lastWeeks = (numberOfWeeks: number) => {
|
||||
const now = new Date();
|
||||
const endDate = startOfDay(subDays(now, 1));
|
||||
const startDate = startOfDay(subDays(endDate, 7 * numberOfWeeks));
|
||||
const endDate = subtractTime(startOfDay(now), 1, "day");
|
||||
const startDate = subtractTime(endDate, 7 * numberOfWeeks, "day");
|
||||
return { startDate, endDate };
|
||||
};
|
||||
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
import { workspaces } from "api/queries/workspaces";
|
||||
import type { Template, Workspace } from "api/typesGenerated";
|
||||
import { compareAsc } from "date-fns";
|
||||
import dayjs from "dayjs";
|
||||
import { useQuery } from "react-query";
|
||||
import type { TemplateScheduleFormValues } from "./formHelpers";
|
||||
|
||||
@@ -29,7 +29,10 @@ export const useWorkspacesToGoDormant = (
|
||||
formValues.time_til_dormant_ms,
|
||||
);
|
||||
|
||||
if (compareAsc(proposedLocking, fromDate) < 1) {
|
||||
if (
|
||||
dayjs(proposedLocking).isBefore(dayjs(fromDate)) ||
|
||||
dayjs(proposedLocking).isSame(dayjs(fromDate))
|
||||
) {
|
||||
return workspace;
|
||||
}
|
||||
});
|
||||
@@ -56,7 +59,10 @@ export const useWorkspacesToBeDeleted = (
|
||||
formValues.time_til_dormant_autodelete_ms,
|
||||
);
|
||||
|
||||
if (compareAsc(proposedLocking, fromDate) < 1) {
|
||||
if (
|
||||
dayjs(proposedLocking).isBefore(dayjs(fromDate)) ||
|
||||
dayjs(proposedLocking).isSame(dayjs(fromDate))
|
||||
) {
|
||||
return workspace;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { timeFrom } from "utils/time";
|
||||
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
@@ -153,9 +154,7 @@ export const AppStatuses: FC<AppStatusesProps> = ({
|
||||
{latestStatus.message}
|
||||
</span>
|
||||
<span className="text-xs text-content-secondary first-letter:uppercase block pl-[26px]">
|
||||
{formatDistance(new Date(latestStatus.created_at), comparisonDate, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
{timeFrom(new Date(latestStatus.created_at), comparisonDate)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -216,13 +215,7 @@ export const AppStatuses: FC<AppStatusesProps> = ({
|
||||
{displayStatuses &&
|
||||
otherStatuses.map((status) => {
|
||||
const statusTime = new Date(status.created_at);
|
||||
const formattedTimestamp = formatDistance(
|
||||
statusTime,
|
||||
comparisonDate,
|
||||
{
|
||||
addSuffix: true,
|
||||
},
|
||||
);
|
||||
const formattedTimestamp = timeFrom(statusTime, comparisonDate);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -7,13 +7,15 @@ import type {
|
||||
WorkspaceBuild,
|
||||
} from "api/typesGenerated";
|
||||
import { MemoizedInlineMarkdown } from "components/Markdown/Markdown";
|
||||
import formatDistanceToNow from "date-fns/formatDistanceToNow";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { TriangleAlertIcon } from "lucide-react";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import { TemplateUpdateMessage } from "modules/templates/TemplateUpdateMessage";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
import { useQuery } from "react-query";
|
||||
import type { WorkspacePermissions } from "../../../modules/workspaces/permissions";
|
||||
import {
|
||||
@@ -147,19 +149,18 @@ export const WorkspaceNotifications: FC<WorkspaceNotificationsProps> = ({
|
||||
detail: workspace.deleting_at ? (
|
||||
<>
|
||||
This workspace has not been used for{" "}
|
||||
{formatDistanceToNow(Date.parse(workspace.last_used_at))} and was
|
||||
marked dormant on {formatDate(workspace.dormant_at, false)}. It is
|
||||
scheduled to be deleted on {formatDate(workspace.deleting_at, true)}.
|
||||
To keep it you must activate the workspace.
|
||||
{dayjs(workspace.last_used_at).fromNow(true)} and was marked dormant
|
||||
on {formatDate(workspace.dormant_at, false)}. It is scheduled to be
|
||||
deleted on {formatDate(workspace.deleting_at, true)}. To keep it you
|
||||
must activate the workspace.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
This workspace has not been used for{" "}
|
||||
{formatDistanceToNow(Date.parse(workspace.last_used_at))} and was
|
||||
marked dormant on {formatDate(workspace.dormant_at, false)}. It is not
|
||||
scheduled for auto-deletion but will become a candidate if
|
||||
auto-deletion is enabled on this template. To keep it you must
|
||||
activate the workspace.
|
||||
{dayjs(workspace.last_used_at).fromNow(true)} and was marked dormant
|
||||
on {formatDate(workspace.dormant_at, false)}. It is not scheduled for
|
||||
auto-deletion but will become a candidate if auto-deletion is enabled
|
||||
on this template. To keep it you must activate the workspace.
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { expect, screen, userEvent, waitFor, within } from "@storybook/test";
|
||||
import { getWorkspaceQuotaQueryKey } from "api/queries/workspaceQuota";
|
||||
import type { Workspace, WorkspaceQuota } from "api/typesGenerated";
|
||||
import { addHours, addMinutes } from "date-fns";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
MockOrganization,
|
||||
MockTemplate,
|
||||
@@ -72,7 +72,7 @@ export const ReadyWithDeadline: Story = {
|
||||
latest_build: {
|
||||
...MockWorkspace.latest_build,
|
||||
get deadline() {
|
||||
return addHours(new Date(), 8).toISOString();
|
||||
return dayjs().add(8, "hour").toISOString();
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -89,7 +89,7 @@ export const Connected: Story = {
|
||||
latest_build: {
|
||||
...MockWorkspace.latest_build,
|
||||
get deadline() {
|
||||
return addHours(new Date(), 8).toISOString();
|
||||
return dayjs().add(8, "hour").toISOString();
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -123,10 +123,10 @@ export const ConnectedWithMaxDeadline: Story = {
|
||||
latest_build: {
|
||||
...MockWorkspace.latest_build,
|
||||
get deadline() {
|
||||
return addHours(new Date(), 1).toISOString();
|
||||
return dayjs().add(1, "hour").toISOString();
|
||||
},
|
||||
get max_deadline() {
|
||||
return addHours(new Date(), 8).toISOString();
|
||||
return dayjs().add(8, "hour").toISOString();
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -160,10 +160,10 @@ export const ConnectedWithMaxDeadlineSoon: Story = {
|
||||
latest_build: {
|
||||
...MockWorkspace.latest_build,
|
||||
get deadline() {
|
||||
return addHours(new Date(), 1).toISOString();
|
||||
return dayjs().add(1, "hour").toISOString();
|
||||
},
|
||||
get max_deadline() {
|
||||
return addHours(new Date(), 1).toISOString();
|
||||
return dayjs().add(1, "hour").toISOString();
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -202,7 +202,7 @@ export const WithApproachingDeadline: Story = {
|
||||
latest_build: {
|
||||
...MockWorkspace.latest_build,
|
||||
get deadline() {
|
||||
return addMinutes(new Date(), 30).toISOString();
|
||||
return dayjs().add(30, "minute").toISOString();
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -228,7 +228,7 @@ export const WithFarAwayDeadline: Story = {
|
||||
latest_build: {
|
||||
...MockWorkspace.latest_build,
|
||||
get deadline() {
|
||||
return addHours(new Date(), 8).toISOString();
|
||||
return dayjs().add(8, "hour").toISOString();
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -254,7 +254,7 @@ export const WithFarAwayDeadlineRequiredByTemplate: Story = {
|
||||
latest_build: {
|
||||
...MockWorkspace.latest_build,
|
||||
get deadline() {
|
||||
return addHours(new Date(), 8).toISOString();
|
||||
return dayjs().add(8, "hour").toISOString();
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+2
-2
@@ -236,9 +236,9 @@ describe("ttlShutdownAt", () => {
|
||||
"Your workspace will shut down 12 minutes after its next start.",
|
||||
],
|
||||
[
|
||||
"48.258 hours --> helper text shows shutdown after 2 days and 15 minutes and 28 seconds",
|
||||
"48.258 hours --> helper text shows shutdown after 2 days, 15 minutes and 29 seconds",
|
||||
48.258,
|
||||
"Your workspace will shut down 2 days and 15 minutes and 28 seconds after its next start.",
|
||||
"Your workspace will shut down 2 days, 15 minutes and 29 seconds after its next start.",
|
||||
],
|
||||
])("%p", (_, ttlHours, expected) => {
|
||||
expect(ttlShutdownAt(ttlHours)).toEqual(expected);
|
||||
|
||||
+6
-19
@@ -21,13 +21,8 @@ import {
|
||||
StackLabel,
|
||||
StackLabelHelperText,
|
||||
} from "components/StackLabel/StackLabel";
|
||||
import { formatDuration, intervalToDuration } from "date-fns";
|
||||
import dayjs from "dayjs";
|
||||
import advancedFormat from "dayjs/plugin/advancedFormat";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { type FormikTouched, useFormik } from "formik";
|
||||
import {
|
||||
defaultSchedule,
|
||||
@@ -35,15 +30,11 @@ import {
|
||||
} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule";
|
||||
import type { ChangeEvent, FC } from "react";
|
||||
import { getFormHelpers } from "utils/formUtils";
|
||||
import { humanDuration } from "utils/time";
|
||||
import { timeZones } from "utils/timeZones";
|
||||
import * as Yup from "yup";
|
||||
|
||||
// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
|
||||
// sorted alphabetically.
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(advancedFormat);
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
// Need dayjs.tz functions for timezone validation
|
||||
dayjs.extend(timezone);
|
||||
|
||||
export const Language = {
|
||||
@@ -163,6 +154,7 @@ export const validationSchema = Yup.object({
|
||||
// Unfortunately, there's not a good API on dayjs at this time for
|
||||
// evaluating a timezone. Attempt to parse today in the supplied timezone
|
||||
// and return as valid if the function doesn't throw.
|
||||
// Need to use dayjs.tz directly here as our utility functions don't expose validation
|
||||
try {
|
||||
dayjs.tz(dayjs(), value);
|
||||
return true;
|
||||
@@ -394,10 +386,8 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
<>
|
||||
Set how many hours should elapse after the workspace started before
|
||||
the workspace automatically shuts down. This will be extended by{" "}
|
||||
{dayjs
|
||||
.duration({ milliseconds: template.activity_bump_ms })
|
||||
.humanize()}{" "}
|
||||
after last activity in the workspace was detected.
|
||||
{humanDuration(template.activity_bump_ms)} after last activity in
|
||||
the workspace was detected.
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -471,10 +461,7 @@ export const ttlShutdownAt = (formTTL: number): string => {
|
||||
}
|
||||
|
||||
try {
|
||||
return `Your workspace will shut down ${formatDuration(
|
||||
intervalToDuration({ start: 0, end: formTTL * 60 * 60 * 1000 }),
|
||||
{ delimiter: " and " },
|
||||
)} after its next start.`;
|
||||
return `Your workspace will shut down ${humanDuration(formTTL * 60 * 60 * 1000)} after its next start.`;
|
||||
} catch (e) {
|
||||
if (e instanceof RangeError) {
|
||||
return Language.errorTtlMax;
|
||||
|
||||
+102
-24
@@ -1,24 +1,70 @@
|
||||
import dayjs from "dayjs";
|
||||
import dayjs, { type Dayjs } from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import DayJSRelativeTime from "dayjs/plugin/relativeTime";
|
||||
import relativeTimePlugin from "dayjs/plugin/relativeTime";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
|
||||
// Load required plugins
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(DayJSRelativeTime);
|
||||
dayjs.extend(relativeTimePlugin);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
// Time conversion constants
|
||||
const TIME_CONSTANTS = {
|
||||
MS_PER_SECOND: 1000,
|
||||
MS_PER_MINUTE: 60 * 1000,
|
||||
MS_PER_HOUR: 60 * 60 * 1000,
|
||||
MS_PER_DAY: 24 * 60 * 60 * 1000,
|
||||
NS_PER_MS: 1e6,
|
||||
SECONDS_PER_MINUTE: 60,
|
||||
MINUTES_PER_HOUR: 60,
|
||||
HOURS_PER_DAY: 24,
|
||||
};
|
||||
|
||||
export type TimeUnit = "days" | "hours";
|
||||
export type DateTimeInput = Date | string | number | Dayjs | null | undefined;
|
||||
|
||||
// Standard format strings
|
||||
// https://day.js.org/docs/en/display/format
|
||||
export const DATE_FORMAT = {
|
||||
ISO_DATE: "YYYY-MM-DD",
|
||||
ISO_DATETIME: "YYYY-MM-DD HH:mm:ss",
|
||||
FULL_DATE: "MMMM D, YYYY",
|
||||
MEDIUM_DATE: "MMM D, YYYY",
|
||||
FULL_DATETIME: "MMMM D, YYYY h:mm A",
|
||||
SHORT_DATE: "MM/DD/YYYY",
|
||||
TIME_24H: "HH:mm:ss",
|
||||
TIME_12H: "h:mm A",
|
||||
UTC_OFFSET: "Z",
|
||||
} as const;
|
||||
|
||||
// Format functions
|
||||
export function formatDateTime(
|
||||
date: DateTimeInput,
|
||||
format: string = DATE_FORMAT.ISO_DATETIME,
|
||||
) {
|
||||
return dayjs(date).format(format);
|
||||
}
|
||||
|
||||
// Duration functions
|
||||
export function humanDuration(durationInMs: number) {
|
||||
if (durationInMs === 0) {
|
||||
return "0 hours";
|
||||
}
|
||||
return humanizeDuration(durationInMs, {
|
||||
conjunction: " and ",
|
||||
serialComma: false,
|
||||
round: true,
|
||||
units: ["y", "mo", "w", "d", "h", "m", "s", "ms"],
|
||||
largest: 3,
|
||||
});
|
||||
}
|
||||
|
||||
const timeUnit = suggestedTimeUnit(durationInMs);
|
||||
const durationValue =
|
||||
timeUnit === "days"
|
||||
? durationInDays(durationInMs)
|
||||
: durationInHours(durationInMs);
|
||||
export function durationInHours(durationMs: number): number {
|
||||
return durationMs / TIME_CONSTANTS.MS_PER_HOUR;
|
||||
}
|
||||
|
||||
return `${durationValue} ${timeUnit}`;
|
||||
export function durationInDays(durationMs: number): number {
|
||||
return durationMs / TIME_CONSTANTS.MS_PER_DAY;
|
||||
}
|
||||
|
||||
export function suggestedTimeUnit(duration: number): TimeUnit {
|
||||
@@ -29,20 +75,52 @@ export function suggestedTimeUnit(duration: number): TimeUnit {
|
||||
return Number.isInteger(durationInDays(duration)) ? "days" : "hours";
|
||||
}
|
||||
|
||||
export function durationInHours(duration: number): number {
|
||||
return duration / 1000 / 60 / 60;
|
||||
}
|
||||
|
||||
export function durationInDays(duration: number): number {
|
||||
return duration / 1000 / 60 / 60 / 24;
|
||||
}
|
||||
|
||||
export function relativeTime(date: Date) {
|
||||
// Relative time functions
|
||||
export function relativeTime(date: DateTimeInput) {
|
||||
return dayjs(date).fromNow();
|
||||
}
|
||||
|
||||
export function relativeTimeWithoutSuffix(date: DateTimeInput) {
|
||||
return dayjs(date).fromNow(true);
|
||||
}
|
||||
|
||||
export function timeFrom(
|
||||
date: DateTimeInput,
|
||||
referenceDate: DateTimeInput = new Date(),
|
||||
) {
|
||||
return dayjs(date).from(dayjs(referenceDate));
|
||||
}
|
||||
|
||||
// Time manipulation functions
|
||||
export function addTime(
|
||||
date: DateTimeInput,
|
||||
amount: number,
|
||||
unit: dayjs.ManipulateType,
|
||||
) {
|
||||
return dayjs(date).add(amount, unit).toDate();
|
||||
}
|
||||
|
||||
export function subtractTime(
|
||||
date: DateTimeInput,
|
||||
amount: number,
|
||||
unit: dayjs.ManipulateType,
|
||||
) {
|
||||
return dayjs(date).subtract(amount, unit).toDate();
|
||||
}
|
||||
|
||||
export function startOfDay(date: DateTimeInput) {
|
||||
return dayjs(date).startOf("day").toDate();
|
||||
}
|
||||
|
||||
export function startOfHour(date: DateTimeInput) {
|
||||
return dayjs(date).startOf("hour").toDate();
|
||||
}
|
||||
|
||||
export function daysAgo(count: number) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - count);
|
||||
return date.toISOString();
|
||||
return dayjs().subtract(count, "day").toISOString();
|
||||
}
|
||||
|
||||
// Date comparison functions
|
||||
export function isAfter(date1: DateTimeInput, date2: DateTimeInput) {
|
||||
return dayjs(date1).isAfter(dayjs(date2));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user