Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add monthly report support #6508

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
{:else if isSuccess}
{#if parseErrors && parseErrors.length > 0}
<ul class="border rounded-sm">
{#each parseErrors as error (error.message)}
{#each parseErrors as error, i (i)}
<li
class="flex gap-x-5 justify-between py-1 px-9 border-b border-gray-200 bg-red-50 font-mono last:border-b-0"
>
Expand Down
4 changes: 3 additions & 1 deletion web-common/src/components/forms/Input.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@

$: type = secret && !showPassword ? "password" : inputType;

$: hasValue = inputType === "number" ? !!value || value === 0 : !!value;

onMount(() => {
if (claimFocusOnMount) {
if (inputElement) {
Expand Down Expand Up @@ -228,7 +230,7 @@
/>
{/if}

{#if errors && (alwaysShowError || (!focus && value))}
{#if errors && (alwaysShowError || (!focus && hasValue))}
{#if typeof errors === "string"}
<div in:slide={{ duration: 200 }} class="error">
{errors}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import Input from "../../components/forms/Input.svelte";
import Select from "../../components/forms/Select.svelte";
import { runtime } from "../../runtime-client/runtime-store";
import { makeTimeZoneOptions } from "./time-utils";
import { makeTimeZoneOptions, ReportFrequency } from "./time-utils";

export let formId: string;
export let data: Readable<ReportValues>;
Expand Down Expand Up @@ -49,12 +49,12 @@
bind:value={$data["frequency"]}
id="frequency"
label="Frequency"
options={["Daily", "Weekdays", "Weekly"].map((frequency) => ({
options={["Daily", "Weekdays", "Weekly", "Monthly"].map((frequency) => ({
value: frequency,
label: frequency,
}))}
/>
{#if $data["frequency"] === "Weekly"}
{#if $data["frequency"] === ReportFrequency.Weekly}
<Select
bind:value={$data["dayOfWeek"]}
id="dayOfWeek"
Expand All @@ -73,6 +73,17 @@
}))}
/>
{/if}
{#if $data["frequency"] === ReportFrequency.Monthly}
<Input
bind:value={$data["dayOfMonth"]}
errors={$errors["dayOfMonth"]}
id="dayOfMonth"
label="Day"
inputType="number"
width="64px"
labelGap={2}
/>
{/if}
<TimePicker bind:value={$data["timeOfDay"]} id="timeOfDay" label="Time" />
<Select
bind:value={$data["timeZone"]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
type ReportValues,
} from "@rilldata/web-common/features/scheduled-reports/utils";
import { defaults, superForm } from "sveltekit-superforms";
import { array, object, string } from "yup";
import { array, object, string, number } from "yup";
import { type ValidationAdapter, yup } from "sveltekit-superforms/adapters";
import { Button } from "../../components/button";
import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus";
Expand Down Expand Up @@ -55,6 +55,8 @@
const schema = yup(
object({
title: string().required("Required"),
// There isnt enough space so just say "Invalid"
dayOfMonth: number().min(1, "Invalid").max(31, "Invalid"),
emailRecipients: array().of(string().email("Invalid email")),
slackChannels: array().of(string()),
slackUsers: array().of(string().email("Invalid email")),
Expand All @@ -66,6 +68,7 @@
values.frequency,
values.dayOfWeek,
values.timeOfDay,
values.dayOfMonth,
);

try {
Expand Down
44 changes: 35 additions & 9 deletions web-common/src/features/scheduled-reports/time-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@ import {
getUTCIANA,
} from "../../lib/time/timezone";

export enum ReportFrequency {
Daily = "Daily",
Weekdays = "Weekdays",
Weekly = "Weekly",
Monthly = "Monthly",
Custom = "Custom",
}

export function getTodaysDayOfWeek(): string {
return DateTime.now().toLocaleString({ weekday: "long" });
}

export function getTodaysDayOfMonth(): number {
return DateTime.now().day;
}

export function getNextQuarterHour(): DateTime {
const now = DateTime.local();
const nextQuarter = now.plus({ minutes: 15 - (now.minute % 15) });
Expand All @@ -20,21 +32,22 @@ export function getTimeIn24FormatFromDateTime(dateTime: DateTime): string {
}

export function convertFormValuesToCronExpression(
frequency: string,
frequency: ReportFrequency,
dayOfWeek: string,
timeOfDay: string,
dayOfMonth: number,
): string {
const [hour, minute] = timeOfDay.split(":").map(Number);
let cronExpr = `${minute} ${hour} `;

switch (frequency) {
case "Daily":
case ReportFrequency.Daily:
cronExpr += "* * *";
break;
case "Weekdays":
case ReportFrequency.Weekdays:
cronExpr += "* * 1-5";
break;
case "Weekly": {
case ReportFrequency.Weekly: {
const weekDayMap: Record<string, number> = {
Sunday: 0,
Monday: 1,
Expand All @@ -47,25 +60,33 @@ export function convertFormValuesToCronExpression(
cronExpr += `* * ${weekDayMap[dayOfWeek]}`;
break;
}
case ReportFrequency.Monthly:
cronExpr += `${dayOfMonth} * *`;
break;
}

return cronExpr;
}

export function getFrequencyFromCronExpression(cronExpr: string): string {
export function getFrequencyFromCronExpression(
cronExpr: string,
): ReportFrequency {
const [, , dayOfMonth, month, dayOfWeek] = cronExpr.split(" ");

if (dayOfMonth === "*" && month === "*") {
if (dayOfWeek === "*") {
return "Daily";
return ReportFrequency.Daily;
} else if (dayOfWeek === "1-5") {
return "Weekdays";
return ReportFrequency.Weekdays;
} else {
return "Weekly";
return ReportFrequency.Weekly;
}
}
if (month === "*" && dayOfWeek === "*") {
return ReportFrequency.Monthly;
}

return "Custom";
return ReportFrequency.Custom;
}

export function getDayOfWeekFromCronExpression(cronExpr: string): string {
Expand Down Expand Up @@ -96,6 +117,11 @@ export function getTimeOfDayFromCronExpression(cronExpr: string): string {
return `${hour}:${minute === "0" ? "00" : minute}`;
}

export function getDayOfMonthFromCronExpression(cronExpr: string): number {
const [, , dayOfMonth] = cronExpr.split(" ");
return Number(dayOfMonth);
}

export function makeTimeZoneOptions(availableTimeZones: string[] | undefined) {
const userLocalIANA = getLocalIANA();
const UTCIana = getUTCIANA();
Expand Down
10 changes: 9 additions & 1 deletion web-common/src/features/scheduled-reports/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { getExploreName } from "@rilldata/web-admin/features/dashboards/query-mappers/utils";
import {
getDayOfMonthFromCronExpression,
getDayOfWeekFromCronExpression,
getFrequencyFromCronExpression,
getNextQuarterHour,
getTimeIn24FormatFromDateTime,
getTimeOfDayFromCronExpression,
getTodaysDayOfMonth,
getTodaysDayOfWeek,
ReportFrequency,
} from "@rilldata/web-common/features/scheduled-reports/time-utils";
import { getLocalIANA } from "@rilldata/web-common/lib/time/timezone";
import {
Expand All @@ -24,12 +27,17 @@ export function getInitialValues(
? getFrequencyFromCronExpression(
reportSpec.refreshSchedule?.cron as string,
)
: "Weekly",
: ReportFrequency.Weekly,
dayOfWeek: reportSpec
? getDayOfWeekFromCronExpression(
reportSpec.refreshSchedule?.cron as string,
)
: getTodaysDayOfWeek(),
dayOfMonth: reportSpec
? getDayOfMonthFromCronExpression(
reportSpec.refreshSchedule?.cron as string,
)
: getTodaysDayOfMonth(),
timeOfDay: reportSpec
? getTimeOfDayFromCronExpression(
reportSpec.refreshSchedule?.cron as string,
Expand Down
Loading