Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Fix parent updates
  • Loading branch information
BrunoQuaresma committed May 9, 2024
commit 09ddb8f1d59c7a4468fd5ad59dd478039a4da4d8
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const meta: Meta<typeof DurationField> = {
label: "Duration",
},
render: function RenderComponent(args) {
const [value, setValue] = useState<number | undefined>(args.value);
const [value, setValue] = useState<number>(args.value);
return (
<DurationField
{...args}
Expand All @@ -23,12 +23,6 @@ const meta: Meta<typeof DurationField> = {
export default meta;
type Story = StoryObj<typeof DurationField>;

export const Empty: Story = {
args: {
value: undefined,
},
};

export const Hours: Story = {
args: {
value: hoursToMs(16),
Expand Down
128 changes: 81 additions & 47 deletions site/src/components/DurationField/DurationField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,39 @@ import FormHelperText from "@mui/material/FormHelperText";
import MenuItem from "@mui/material/MenuItem";
import Select from "@mui/material/Select";
import TextField from "@mui/material/TextField";
import { type ReactNode, useState, type FC } from "react";
import { type ReactNode, useState, type FC, useEffect } from "react";

type TimeUnit = "days" | "hours";

// Value should be in milliseconds or undefined. Undefined means no value.
type DurationValue = number | undefined;

type DurationFieldProps = {
label: string;
value: DurationValue;
// Value is in ms
value: number;
disabled?: boolean;
helperText?: ReactNode;
onChange: (value: DurationValue) => void;
onChange: (value: number) => void;
};

type State = {
unit: TimeUnit;
// Handling empty values as strings in the input simplifies the process,
// especially when a user clears the input field.
durationFieldValue: string;
};

export const DurationField: FC<DurationFieldProps> = (props) => {
const { label, value, disabled, helperText, onChange } = props;
const [timeUnit, setTimeUnit] = useState<TimeUnit>(() => {
if (!value) {
return "hours";
}
const { label, value: parentValue, disabled, helperText, onChange } = props;
const [state, setState] = useState<State>(() => initState(parentValue));
const currentDurationInMs = durationInMs(
state.durationFieldValue,
state.unit,
);

return Number.isInteger(durationToDays(value)) ? "days" : "hours";
});
useEffect(() => {
if (parentValue !== currentDurationInMs) {
setState(initState(parentValue));
}
}, [currentDurationInMs, parentValue]);

return (
<div>
Expand All @@ -41,32 +50,22 @@ export const DurationField: FC<DurationFieldProps> = (props) => {
css={{ maxWidth: 160 }}
label={label}
disabled={disabled}
value={
!value
? ""
: timeUnit === "hours"
? durationToHours(value)
: durationToDays(value)
}
value={state.durationFieldValue}
onChange={(e) => {
if (e.target.value === "") {
onChange(undefined);
}

let value = parseInt(e.target.value);

if (Number.isNaN(value)) {
return;
}
const durationFieldValue = e.currentTarget.value;

// Avoid negative values
value = Math.abs(value);
setState((state) => ({
...state,
durationFieldValue,
}));

onChange(
timeUnit === "hours"
? hoursToDuration(value)
: daysToDuration(value),
const newDurationInMs = durationInMs(
durationFieldValue,
state.unit,
);
if (newDurationInMs !== parentValue) {
onChange(newDurationInMs);
}
}}
inputProps={{
step: 1,
Expand All @@ -75,22 +74,29 @@ export const DurationField: FC<DurationFieldProps> = (props) => {
<Select
disabled={disabled}
css={{ width: 120, "& .MuiSelect-icon": { padding: 2 } }}
value={timeUnit}
value={state.unit}
onChange={(e) => {
setTimeUnit(e.target.value as TimeUnit);
const unit = e.target.value as TimeUnit;
setState(() => ({
unit,
durationFieldValue:
unit === "hours"
? durationInHours(currentDurationInMs).toString()
: durationInDays(currentDurationInMs).toString(),
}));
}}
inputProps={{ "aria-label": "Time unit" }}
IconComponent={KeyboardArrowDown}
>
<MenuItem
value="hours"
disabled={Boolean(value && !canConvertDurationToHours(value))}
disabled={!canConvertDurationToHours(currentDurationInMs)}
>
Hours
</MenuItem>
<MenuItem
value="days"
disabled={Boolean(value && !canConvertDurationToDays(value))}
disabled={!canConvertDurationToDays(currentDurationInMs)}
>
Days
</MenuItem>
Expand All @@ -102,26 +108,54 @@ export const DurationField: FC<DurationFieldProps> = (props) => {
);
};

function durationToHours(duration: number): number {
return duration / 1000 / 60 / 60;
function initState(value: number): State {
const unit = suggestedTimeUnit(value);
const durationFieldValue =
unit === "hours"
? durationInHours(value).toString()
: durationInDays(value).toString();

return {
unit,
durationFieldValue,
};
}

function durationInMs(durationFieldValue: string, unit: TimeUnit): number {
const durationInMs = parseInt(durationFieldValue);
return unit === "hours"
? hoursToDuration(durationInMs)
: daysToDuration(durationInMs);
}

function hoursToDuration(hours: number): number {
return hours * 60 * 60 * 1000;
}

function durationToDays(duration: number): number {
return duration / 1000 / 60 / 60 / 24;
function daysToDuration(days: number): number {
return days * 24 * hoursToDuration(1);
}

function daysToDuration(days: number): number {
return days * 24 * 60 * 60 * 1000;
function suggestedTimeUnit(duration: number): TimeUnit {
if (duration === 0) {
return "hours";
}

return Number.isInteger(durationInDays(duration)) ? "days" : "hours";
}

function durationInHours(duration: number): number {
return duration / 1000 / 60 / 60;
}

function durationInDays(duration: number): number {
return duration / 1000 / 60 / 60 / 24;
}

function canConvertDurationToDays(duration: number): boolean {
return Number.isInteger(durationToDays(duration));
return Number.isInteger(durationInDays(duration));
}

function canConvertDurationToHours(duration: number): boolean {
return Number.isInteger(durationToHours(duration));
return Number.isInteger(durationInHours(duration));
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
const MS_HOUR_CONVERSION = 3600000;
const MS_DAY_CONVERSION = 86400000;
const FAILURE_CLEANUP_DEFAULT = 7;
const INACTIVITY_CLEANUP_DEFAULT = 180;
const INACTIVITY_CLEANUP_DEFAULT = 180 * MS_DAY_CONVERSION;
const DORMANT_AUTODELETION_DEFAULT = 30;
/**
* The default form field space is 4 but since this form is quite heavy I think
Expand Down Expand Up @@ -496,31 +496,14 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
label={<StackLabel>Enable Dormancy Threshold</StackLabel>}
/>

{/* <TextField
{...getFieldHelpers("time_til_dormant_ms", {
helperText: (
<DormancyTTLHelperText
ttl={form.values.time_til_dormant_ms}
/>
),
})}
disabled={
isSubmitting || !form.values.inactivity_cleanup_enabled
}
fullWidth
inputProps={{ min: 0, step: "any" }}
label="Time until dormant (days)"
type="number"
/> */}

<DurationField
label="Time until dormant"
helperText={
<DormancyTTLHelperText
ttl={form.values.time_til_dormant_ms}
/>
}
value={form.values.time_til_dormant_ms}
value={form.values.time_til_dormant_ms ?? 0}
onChange={(v) => form.setFieldValue("time_til_dormant_ms", v)}
disabled={
isSubmitting || !form.values.inactivity_cleanup_enabled
Expand Down