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(dashboard): scheduled digest #7314

Open
wants to merge 6 commits into
base: next
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {

const DigestRegularControlZodSchema = z
.object({
amount: z.union([z.number().min(1), z.string()]),
amount: z.union([z.number().min(1), z.string().min(1)]),
unit: z.nativeEnum(TimeUnitEnum).default(TimeUnitEnum.SECONDS),
digestKey: z.string().optional(),
lookBackWindow: z
Expand All @@ -26,7 +26,7 @@ const DigestRegularControlZodSchema = z

const DigestTimedControlZodSchema = z
.object({
cron: z.string(),
cron: z.string().min(1),
digestKey: z.string().optional(),
})
.strict();
Expand Down Expand Up @@ -72,7 +72,7 @@ export const digestUiSchema: UiSchema = {
},
cron: {
component: UiComponentEnum.DIGEST_CRON,
placeholder: null,
placeholder: '',
},
},
};
21 changes: 10 additions & 11 deletions apps/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the font used for the amount input

rel="stylesheet"
/>
<% if (env.VITE_GTM) { %>
Expand Down Expand Up @@ -51,16 +51,15 @@
async="async"
type="text/javascript"
></script>
<% } %>
<% if (env.VITE_PLAIN_SUPPORT_CHAT_APP_ID) { %>
<script>
(function(d, script) {
script = d.createElement('script');
script.async = false;
script.src = 'https://chat.cdn-plain.com/index.js';
d.getElementsByTagName('head')[0].appendChild(script);
}(document));
</script>
<% } %> <% if (env.VITE_PLAIN_SUPPORT_CHAT_APP_ID) { %>
<script>
(function (d, script) {
script = d.createElement('script');
script.async = false;
script.src = 'https://chat.cdn-plain.com/index.js';
d.getElementsByTagName('head')[0].appendChild(script);
})(document);
</script>
<% } %>
<div id="root" class="h-full"></div>
<script type="module" src="/src/main.tsx"></script>
Expand Down
3 changes: 3 additions & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@sentry/react": "^8.35.0",
"@tanstack/react-query": "^5.59.6",
"@types/js-cookie": "^3.0.6",
"@types/lodash.isequal": "^4.5.8",
"@uiw/codemirror-extensions-langs": "^4.23.6",
"@uiw/codemirror-theme-material": "^4.23.6",
"@uiw/codemirror-theme-white": "^4.23.6",
Expand All @@ -69,11 +70,13 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"cron-parser": "^4.9.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the library used to parse and generate cron expression

"date-fns": "^4.1.0",
"flat": "^6.0.1",
"js-cookie": "^3.0.5",
"launchdarkly-react-client-sdk": "^3.3.2",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"lodash.merge": "^4.6.2",
"lucide-react": "^0.439.0",
"mixpanel-browser": "^2.52.0",
Expand Down
179 changes: 131 additions & 48 deletions apps/dashboard/src/components/amount-input.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { FocusEventHandler } from 'react';
import { useFormContext } from 'react-hook-form';

import { cn } from '@/utils/ui';
import { FormControl, FormField, FormItem, FormMessagePure } from '@/components/primitives/form/form';
import { Input } from '@/components/primitives/input';
import { InputFieldPure } from '@/components/primitives/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/primitives/select';
import { useFormContext } from 'react-hook-form';
import { AUTOCOMPLETE_PASSWORD_MANAGERS_OFF } from '@/utils/constants';

const HEIGHT = {
Expand All @@ -22,18 +24,124 @@ type InputWithSelectProps = {
inputKey: string;
selectKey: string;
};
options: string[];
options: Array<{ label: string; value: string }>;
defaultOption?: string;
className?: string;
placeholder?: string;
isReadOnly?: boolean;
onValueChange?: (value: string) => void;
onValueChange?: () => void;
size?: 'sm' | 'md';
min?: number;
showError?: boolean;
shouldUnregister?: boolean;
};

const AmountInputContainer = ({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the changes I made here are: split components into smaller independent pieces so they can be reused without being dependent on the form

children,
className,
size = 'sm',
hasError,
}: {
children?: React.ReactNode | React.ReactNode[];
className?: string;
size?: 'sm' | 'md';
hasError?: boolean;
}) => {
return (
<InputFieldPure
className={cn(HEIGHT[size].base, 'rounded-lg border pr-0', className)}
state={hasError ? 'error' : 'default'}
>
{children}
</InputFieldPure>
);
};

export const AmountInput = ({
const AmountInputField = ({
value,
min,
placeholder,
disabled,
onChange,
onBlur,
}: {
value?: string | number;
placeholder?: string;
disabled?: boolean;
min?: number;
onChange: (arg: string | number) => void;
onBlur?: FocusEventHandler<HTMLInputElement>;
}) => {
return (
<Input
type="number"
className="font-code min-w-[20ch] [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
placeholder={placeholder}
disabled={disabled}
value={value}
onKeyDown={(e) => {
if (e.key === 'e' || e.key === '-' || e.key === '+' || e.key === '.' || e.key === ',') {
e.preventDefault();
}
}}
onChange={(e) => {
if (e.target.value === '') {
onChange('');
return;
}

const numberValue = Number(e.target.value);
onChange(numberValue);
}}
min={min}
onBlur={onBlur}
{...AUTOCOMPLETE_PASSWORD_MANAGERS_OFF}
/>
);
};

const AmountUnitSelect = ({
value,
defaultOption,
options,
size = 'sm',
disabled,
onValueChange,
}: {
value?: string;
defaultOption?: string;
options: Array<{ label: string; value: string }>;
size?: 'sm' | 'md';
disabled?: boolean;
onValueChange?: (val: string) => void;
}) => {
return (
<Select onValueChange={onValueChange} defaultValue={defaultOption} disabled={disabled} value={value}>
<SelectTrigger
className={cn(
HEIGHT[size].trigger,
'w-auto gap-1 rounded-l-none border-x-0 border-y-0 border-l bg-neutral-50 p-2 text-xs'
)}
>
<SelectValue />
</SelectTrigger>
<SelectContent
onBlur={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{options.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
);
};

const AmountInput = ({
fields,
options,
defaultOption,
Expand All @@ -44,6 +152,7 @@ export const AmountInput = ({
size = 'sm',
min,
showError = true,
shouldUnregister = false,
}: InputWithSelectProps) => {
const { getFieldState, setValue, control } = useFormContext();

Expand All @@ -53,38 +162,23 @@ export const AmountInput = ({

return (
<>
<InputFieldPure
className={cn(HEIGHT[size].base, 'rounded-lg border pr-0', className)}
state={input.error ? 'error' : 'default'}
>
<AmountInputContainer className={className} hasError={!!input.error}>
<FormField
control={control}
name={fields.inputKey}
shouldUnregister={shouldUnregister}
render={({ field }) => (
<FormItem className="w-full overflow-hidden">
<FormControl>
<Input
type="number"
className="min-w-[20ch] [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
<AmountInputField
placeholder={placeholder}
disabled={isReadOnly}
value={field.value}
onKeyDown={(e) => {
if (e.key === 'e' || e.key === '-' || e.key === '+' || e.key === '.' || e.key === ',') {
e.preventDefault();
}
}}
onChange={(e) => {
if (e.target.value === '') {
field.onChange('');
return;
}

const numberValue = Number(e.target.value);
field.onChange(numberValue);
onChange={field.onChange}
onBlur={() => {
onValueChange?.();
}}
min={min}
{...AUTOCOMPLETE_PASSWORD_MANAGERS_OFF}
/>
</FormControl>
</FormItem>
Expand All @@ -93,40 +187,29 @@ export const AmountInput = ({
<FormField
control={control}
name={fields.selectKey}
shouldUnregister={shouldUnregister}
render={({ field }) => (
<FormItem>
<FormControl>
<Select
<AmountUnitSelect
value={field.value}
defaultOption={defaultOption}
options={options}
size={size}
disabled={isReadOnly}
onValueChange={(value) => {
setValue(fields.selectKey, value, { shouldDirty: true });
onValueChange?.(value);
onValueChange?.();
}}
defaultValue={defaultOption}
disabled={isReadOnly}
value={field.value}
>
<SelectTrigger
className={cn(
HEIGHT[size].trigger,
'w-auto gap-1 rounded-l-none border-x-0 border-y-0 border-l bg-neutral-50 p-2 text-xs'
)}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
/>
</FormControl>
</FormItem>
)}
/>
</InputFieldPure>
</AmountInputContainer>
{showError && <FormMessagePure error={error ? String(error.message) : undefined} />}
</>
);
};

export { AmountInput, AmountInputContainer, AmountInputField, AmountUnitSelect };
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/primitives/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const FormLabel = React.forwardRef<
>
<BsFillInfoCircleFill className="text-foreground-300 -mt-0.5 inline size-3" />
</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
<TooltipContent className="max-w-56">{tooltip}</TooltipContent>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjust the max width of the tooltip content so it doesn't look weird

</Tooltip>
)}

Expand Down
11 changes: 6 additions & 5 deletions apps/dashboard/src/components/primitives/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,13 @@ const inputFieldVariants = cva(

export type InputFieldPureProps = { children: React.ReactNode; className?: string } & VariantProps<
typeof inputFieldVariants
>;
> &
React.InputHTMLAttributes<HTMLDivElement>;

const InputFieldPure = React.forwardRef<HTMLInputElement, InputFieldPureProps>(
({ children, className, size, state }, ref) => {
const InputFieldPure = React.forwardRef<HTMLDivElement, InputFieldPureProps>(
({ children, className, size, state, ...rest }, ref) => {
return (
<div ref={ref} className={cn(inputFieldVariants({ size, state }), className)}>
<div ref={ref} className={cn(inputFieldVariants({ size, state }), className)} {...rest}>
{children}
</div>
);
Expand All @@ -78,7 +79,7 @@ InputFieldPure.displayName = 'InputFieldPure';

export type InputFieldProps = Omit<InputFieldPureProps, 'state'>;

const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(({ ...props }, ref) => {
const InputField = React.forwardRef<HTMLDivElement, InputFieldProps>(({ ...props }, ref) => {
const { error } = useFormField();

return <InputFieldPure ref={ref} {...props} state={error?.message ? 'error' : 'default'} />;
Expand Down
Loading
Loading