Skip to content

Commit

Permalink
[UI v2] feat: Adds cron input component
Browse files Browse the repository at this point in the history
  • Loading branch information
devinvillarosa committed Feb 11, 2025
1 parent 3c5112d commit b39afff
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 0 deletions.
22 changes: 22 additions & 0 deletions ui-v2/package-lock.json

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

1 change: 1 addition & 0 deletions ui-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
"cron-parser": "^4.9.0",
"cronstrue": "^2.54.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
Expand Down
25 changes: 25 additions & 0 deletions ui-v2/src/components/ui/cron-input/cron-input.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { useState } from "react";
import { CronInput } from "./cron-input";

const meta: Meta<typeof CronInput> = {
title: "UI/CronInput",
render: () => <CronInputStory />,
};

export default meta;

export const story: StoryObj = { name: "CronInput" };

const CronInputStory = () => {
const [input, setInput] = useState("* * * * *");

return (
<CronInput
value={input}
onChange={(e) => setInput(e.target.value)}
getIsCronValid={fn}
/>
);
};
46 changes: 46 additions & 0 deletions ui-v2/src/components/ui/cron-input/cron-input.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { describe, expect, it, vi } from "vitest";
import { CronInput, type CronInputProps } from "./cron-input";

describe("CronInput", () => {
const TestCronInput = ({ getIsCronValid }: CronInputProps) => {
const [value, setValue] = useState("");
return (
<CronInput
value={value}
onChange={(e) => setValue(e.target.value)}
getIsCronValid={getIsCronValid}
/>
);
};

it("renders a valid cron message", async () => {
// SETUP
const user = userEvent.setup();
const mockGetIsCronValid = vi.fn();
render(<TestCronInput getIsCronValid={mockGetIsCronValid} />);

// TEST
await user.type(screen.getByRole("textbox"), "* * * * *");

// ASSERT
expect(screen.getByText("Every minute")).toBeVisible();
expect(mockGetIsCronValid).toHaveBeenLastCalledWith(true);
});

it("renders an valid cron message", async () => {
// SETUP
const user = userEvent.setup();
const mockGetIsCronValid = vi.fn();
render(<TestCronInput getIsCronValid={mockGetIsCronValid} />);

// TEST
await user.type(screen.getByRole("textbox"), "abcd");

// ASSERT
expect(screen.getByText("Invalid expression")).toBeVisible();
expect(mockGetIsCronValid).toHaveBeenLastCalledWith(false);
});
});
64 changes: 64 additions & 0 deletions ui-v2/src/components/ui/cron-input/cron-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Input, type InputProps } from "@/components/ui/input";
import { Typography } from "@/components/ui/typography";
import clsx from "clsx";
import cronParser from "cron-parser";
import cronstrue from "cronstrue";
import { useState } from "react";

const verifyCronValue = (cronValue: string) => {
let description = "";
let isCronValid = false;
try {
cronParser.parseExpression(cronValue);
description = cronstrue.toString(cronValue);
isCronValid = true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
isCronValid = false;
description = "Invalid expression";
}
return {
description,
isCronValid,
};
};

export type CronInputProps = InputProps & {
/** Used to indicate the container if Cron is valid */
getIsCronValid?: (isValid: boolean) => void;
};

export const CronInput = ({
getIsCronValid = () => true,
onChange,
...props
}: CronInputProps) => {
const [description, setDescription] = useState(
verifyCronValue(String(props.value)).description,
);
const [isCronValid, setIsCronValid] = useState(
verifyCronValue(String(props.value)).isCronValid,
);

const handleChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
if (onChange) {
onChange(event);
const { description, isCronValid } = verifyCronValue(event.target.value);
setDescription(description);
setIsCronValid(isCronValid);
getIsCronValid(isCronValid);
}
};

return (
<div className="flex flex-col gap-1">
<Input {...props} onChange={handleChange} />
<Typography
variant="bodySmall"
className={clsx(isCronValid ? "text-muted-foreground" : "text-red-500")}
>
{description}
</Typography>
</div>
);
};
1 change: 1 addition & 0 deletions ui-v2/src/components/ui/cron-input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CronInput } from "./cron-input";

0 comments on commit b39afff

Please sign in to comment.