Skip to content
Merged
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
2 changes: 1 addition & 1 deletion frontend/src/components/v3/generic/Field/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>)
<Label
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit items-center gap-1.5 text-xs leading-snug text-accent group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 [&>*]:data-[slot=field]:p-2.5",
"group/field-label peer/field-label flex w-fit items-center gap-1.5 text-xs leading-snug text-accent group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 [&>*]:data-[slot=field]:p-2.5 [&>svg]:size-3",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
className
)}
Expand Down
15 changes: 12 additions & 3 deletions frontend/src/components/v3/generic/Input/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { forwardRef } from "react";

import { cn } from "../../utils";

function UnstableInput({ className, type, ...props }: React.ComponentProps<"input">) {
const UnstableInput = forwardRef<
HTMLInputElement,
React.ComponentProps<"input"> & { isError?: boolean }
>(({ className, type, isError, ...props }, ref) => {
return (
<input
ref={ref}
type={type}
data-slot="input"
className={cn(
"h-9 w-full min-w-0 rounded-md border border-border bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"aria-invalid:border-danger aria-invalid:ring-danger/40",
"selection:bg-foreground selection:text-background",
className
)}
aria-invalid={isError}
{...props}
/>
);
}
});

UnstableInput.displayName = "UnstableInput";

export { UnstableInput };
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { useMemo, useState } from "react";
import { CheckIcon, CopyIcon, KeyRoundIcon, RefreshCwIcon } from "lucide-react";

import { useTimedReset } from "@app/hooks";

import { Button } from "../Button";
import { Checkbox } from "../Checkbox";
import { UnstableIconButton } from "../IconButton";
import { Label } from "../Label";
import { Popover, PopoverContent, PopoverTrigger } from "../Popover";

type PasswordOptionsType = {
length: number;
useUppercase: boolean;
useLowercase: boolean;
useNumbers: boolean;
useSpecialChars: boolean;
};

export type PasswordGeneratorProps = {
onUsePassword?: (password: string) => void;
isDisabled?: boolean;
minLength?: number;
maxLength?: number;
};

export const PasswordGenerator = ({
onUsePassword,
isDisabled = false,
minLength = 12,
maxLength = 64
}: PasswordGeneratorProps) => {
const [isOpen, setIsOpen] = useState(false);
const [, isCopying, setCopyText] = useTimedReset<string>({
initialState: "Copy"
});
const [refresh, setRefresh] = useState(false);
const [passwordOptions, setPasswordOptions] = useState<PasswordOptionsType>({
length: minLength,
useUppercase: true,
useLowercase: true,
useNumbers: true,
useSpecialChars: true
});

const password = useMemo(() => {
const charset = {
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
lowercase: "abcdefghijklmnopqrstuvwxyz",
numbers: "0123456789",
specialChars: "-_.~!*"
};

let availableChars = "";
if (passwordOptions.useUppercase) availableChars += charset.uppercase;
if (passwordOptions.useLowercase) availableChars += charset.lowercase;
if (passwordOptions.useNumbers) availableChars += charset.numbers;
if (passwordOptions.useSpecialChars) availableChars += charset.specialChars;

if (availableChars === "") availableChars = charset.lowercase + charset.numbers;

const randomBytes = new Uint32Array(passwordOptions.length);
crypto.getRandomValues(randomBytes);
let newPassword = "";
for (let i = 0; i < passwordOptions.length; i += 1) {
newPassword += availableChars[randomBytes[i] % availableChars.length];
}

return newPassword;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [passwordOptions, refresh]);

const copyToClipboard = () => {
navigator.clipboard
.writeText(password)
.then(() => {
setCopyText("Copied");
})
.catch(() => {
setCopyText("Copy failed");
});
};

const usePassword = () => {
onUsePassword?.(password);
setIsOpen(false);
};

return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<UnstableIconButton variant="outline" size="md" isDisabled={isDisabled}>
<KeyRoundIcon />
</UnstableIconButton>
</PopoverTrigger>
<PopoverContent className="w-[30rem]" align="end">
<div className="flex flex-col gap-4">
<div>
<p className="text-sm font-medium">Generate Random Value</p>
<p className="mt-0.5 text-xs text-muted">Generate strong unique values</p>
</div>

<div className="rounded-md border border-border bg-container/50 p-3">
<div className="flex items-center justify-between gap-2">
<p className="flex-1 font-mono text-sm break-all select-all">{password}</p>
<div className="flex shrink-0 gap-1">
<UnstableIconButton
variant="ghost"
size="xs"
onClick={() => setRefresh((prev) => !prev)}
>
<RefreshCwIcon />
</UnstableIconButton>
<UnstableIconButton variant="ghost" size="xs" onClick={copyToClipboard}>
{isCopying ? <CheckIcon /> : <CopyIcon />}
</UnstableIconButton>
</div>
</div>
</div>

<div>
<div className="mb-2 flex items-center justify-between">
<Label className="text-xs text-accent">Length: {passwordOptions.length}</Label>
</div>
<input
type="range"
min={minLength}
max={maxLength}
value={passwordOptions.length}
onChange={(e) =>
setPasswordOptions({ ...passwordOptions, length: Number(e.target.value) })
}
className="h-1.5 w-full cursor-pointer appearance-none rounded-full bg-foreground/10 accent-project [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-foreground [&::-webkit-slider-thumb]:shadow-sm"
/>
</div>

<div className="flex flex-wrap gap-6">
<Label className="flex cursor-pointer items-center gap-1.5 text-xs">
<Checkbox
variant="project"
isChecked={passwordOptions.useUppercase}
onCheckedChange={(checked) =>
setPasswordOptions({ ...passwordOptions, useUppercase: checked as boolean })
}
/>
A-Z
</Label>
<Label className="flex cursor-pointer items-center gap-1.5 text-xs">
<Checkbox
variant="project"
isChecked={passwordOptions.useLowercase}
onCheckedChange={(checked) =>
setPasswordOptions({ ...passwordOptions, useLowercase: checked as boolean })
}
/>
a-z
</Label>
<Label className="flex cursor-pointer items-center gap-1.5 text-xs">
<Checkbox
variant="project"
isChecked={passwordOptions.useNumbers}
onCheckedChange={(checked) =>
setPasswordOptions({ ...passwordOptions, useNumbers: checked as boolean })
}
/>
0-9
</Label>
<Label className="flex cursor-pointer items-center gap-1.5 text-xs">
<Checkbox
variant="project"
isChecked={passwordOptions.useSpecialChars}
onCheckedChange={(checked) =>
setPasswordOptions({ ...passwordOptions, useSpecialChars: checked as boolean })
}
/>
-_.~!*
</Label>
</div>

{onUsePassword && (
<Button variant="project" size="xs" onClick={usePassword} className="w-full">
Use Value
</Button>
)}
</div>
</PopoverContent>
</Popover>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PasswordGenerator, type PasswordGeneratorProps } from "./PasswordGenerator";
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { GroupBase } from "react-select";
import ReactSelectCreatable, { CreatableProps } from "react-select/creatable";

import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "./components";
import { selectClassNames, selectStyles } from "./styles";
import { getSelectClassNames, selectClassNames, selectStyles } from "./styles";

export const CreatableSelect = <T,>({
isMulti,
closeMenuOnSelect,
isError,
...props
}: CreatableProps<T, boolean, GroupBase<T>>) => {
}: CreatableProps<T, boolean, GroupBase<T>> & { isError?: boolean }) => {
return (
<ReactSelectCreatable
isMulti={isMulti}
Expand All @@ -18,7 +19,7 @@ export const CreatableSelect = <T,>({
data-slot="creatable-select"
styles={selectStyles as any}
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
classNames={selectClassNames as any}
classNames={(isError ? getSelectClassNames(isError) : selectClassNames) as any}
{...props}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Select, { GroupBase, Props } from "react-select";

import { ClearIndicator, DropdownIndicator, Group, MultiValueRemove, Option } from "./components";
import { selectClassNames, selectStyles } from "./styles";
import { getSelectClassNames, selectClassNames, selectStyles } from "./styles";

export const FilterableSelect = <T,>({
isMulti,
Expand All @@ -10,10 +10,12 @@ export const FilterableSelect = <T,>({
groupBy = null,
getGroupHeaderLabel = null,
options = [],
isError,
...props
}: Props<T, boolean, GroupBase<T>> & {
groupBy?: string | null;
getGroupHeaderLabel?: ((groupValue: any) => string) | null;
isError?: boolean;
}) => {
let processedOptions: Props<T, boolean, GroupBase<T>>["options"] = options;

Expand Down Expand Up @@ -56,7 +58,7 @@ export const FilterableSelect = <T,>({
Group,
...props.components
}}
classNames={selectClassNames as any}
classNames={(isError ? getSelectClassNames(isError) : selectClassNames) as any}
{...props}
/>
);
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/components/v3/generic/ReactSelect/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,21 @@ export const selectStyles: StylesConfig<unknown, boolean, GroupBase<unknown>> =
zIndex: 99999
})
};

export const getSelectClassNames = (
isError?: boolean
): ClassNamesConfig<unknown, boolean, GroupBase<unknown>> => ({
...selectClassNames,
control: ({ isFocused }) =>
cn(
"!min-h-9 w-full cursor-pointer rounded-md border bg-transparent py-1 pr-1 pl-2 text-sm",
// eslint-disable-next-line no-nested-ternary
isError
? isFocused
? "border-danger ring-[3px] ring-danger/40"
: "border-danger"
: isFocused
? "border-ring ring-[3px] ring-ring/50"
: "border-border hover:border-foreground/20"
)
});
Loading