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
28 changes: 6 additions & 22 deletions src/components/Form/FormInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import type {
GenericFormElementProps,
} from "@components/Form/DynamicForm.tsx";
import { Input } from "@components/UI/Input.tsx";
import type { LucideIcon } from "lucide-react";
import { Eye, EyeOff } from "lucide-react";
import type { ChangeEventHandler } from "react";
import { useState } from "react";
import { useController, type FieldValues } from "react-hook-form";
Expand All @@ -23,10 +21,8 @@ export interface InputFieldProps<T> extends BaseFormBuilderProps<T> {
currentValueLength?: number;
showCharacterCount?: boolean;
},
action?: {
icon: LucideIcon;
onClick: () => void;
};
showPasswordToggle?: boolean;
showCopyButton?: boolean;
};
}

Expand All @@ -36,19 +32,13 @@ export function GenericInput<T extends FieldValues>({
field,
}: GenericFormElementProps<T, InputFieldProps<T>>) {
const { fieldLength, ...restProperties } = field.properties || {};

const [passwordShown, setPasswordShown] = useState(false);
const [currentLength, setCurrentLength] = useState<number>(fieldLength?.currentValueLength || 0);

const { field: controllerField } = useController({
name: field.name,
control,
});

const togglePasswordVisiblity = () => {
setPasswordShown(!passwordShown);
};

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;

Expand All @@ -66,25 +56,19 @@ export function GenericInput<T extends FieldValues>({
return (
<div className="relative w-full">
<Input
type={field.type === "password" && passwordShown ? "text" : field.type}
action={
field.type === "password"
? {
icon: passwordShown ? EyeOff : Eye,
onClick: togglePasswordVisiblity,
}
: undefined
}
type={field.type}
step={field.properties?.step}
value={field.type === "number" ? String(controllerField.value) : controllerField.value}
id={field.name}
onChange={handleInputChange}
showCopyButton={field.properties?.showCopyButton}
showPasswordToggle={field.properties?.showPasswordToggle || field.type === "password"}
{...restProperties}
disabled={disabled}
/>

{fieldLength?.showCharacterCount && fieldLength?.max && (
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-sm text-slate-500 dark:text-slate-400">
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-sm text-slate-900 dark:text-slate-200">
{currentLength ?? fieldLength?.currentValueLength}/{fieldLength?.max}
</div>
)}
Expand Down
22 changes: 8 additions & 14 deletions src/components/Form/FormPasswordGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,52 @@ import type {
} from "@components/Form/DynamicForm.tsx";
import type { ButtonVariant } from "../UI/Button.tsx";
import { Generator } from "@components/UI/Generator.tsx";
import { Eye, EyeOff } from "lucide-react";
import type { ChangeEventHandler } from "react";
import { useState } from "react";
import { Controller, type FieldValues } from "react-hook-form";
import { usePasswordVisibilityToggle } from "@core/hooks/usePasswordVisibilityToggle.ts";

export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
type: "passwordGenerator";
id: string;
hide?: boolean;
bits?: { text: string; value: string; key: string }[];
devicePSKBitCount: number;
inputChange: ChangeEventHandler;
inputChange: ChangeEventHandler<HTMLInputElement> | undefined;
selectChange: (event: string) => void;
actionButtons: {
text: string;
onClick: React.MouseEventHandler<HTMLButtonElement>;
variant: ButtonVariant;
className?: string;
}[];
showPasswordToggle?: boolean;
showCopyButton?: boolean;
}

export function PasswordGenerator<T extends FieldValues>({
control,
field,
disabled,
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) {
const [passwordShown, setPasswordShown] = useState(false);
const togglePasswordVisiblity = () => {
setPasswordShown(!passwordShown);
};
const { isVisible } = usePasswordVisibilityToggle()

return (
<Controller
name={field.name}
control={control}
render={({ field: { value, ...rest } }) => (
<Generator
type={field.hide && !passwordShown ? "password" : "text"}
type={field.hide && !isVisible ? "password" : "text"}
id={field.id}
action={field.hide
? {
icon: passwordShown ? EyeOff : Eye,
onClick: togglePasswordVisiblity,
}
: undefined}
devicePSKBitCount={field.devicePSKBitCount}
bits={field.bits}
inputChange={field.inputChange}
selectChange={field.selectChange}
value={value}
variant={field.validationText ? "invalid" : "default"}
actionButtons={field.actionButtons}
showPasswordToggle={field.showPasswordToggle}
showCopyButton={field.showCopyButton}
{...field.properties}
{...rest}
disabled={disabled}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Form/FormWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ export const FieldWrapper = ({
<div className="pt-6 sm:pt-5">
<fieldset aria-labelledby="label-notifications">
{/* first column = labels/heading, second column = fields, third column = gutter */}
<div className="grid md:grid-cols-[1fr_2fr] lg:grid-cols-[1fr_2fr_1fr] sm:items-baseline gap-4">
<div className="grid grid-cols-1 lg:grid-cols-[0.6fr_2fr_.1fr] sm:items-baseline gap-4">
<Label htmlFor={fieldName}>{label}</Label>
<div className="max-w-3xl">
<p className="text-sm text-slate-500">{description}</p>
<p className="text-sm text-slate-400">{description}</p>
<p hidden={valid ?? true} className="text-sm text-red-500">
{validationText}
</p>
Expand Down
2 changes: 2 additions & 0 deletions src/components/PageComponents/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
hide: true,
properties: {
value: pass,
showPasswordToggle: true,
showCopyButton: true,
},
},
{
Expand Down
9 changes: 4 additions & 5 deletions src/components/PageComponents/Config/Security/Security.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,8 @@ export const Security = () => {
],
properties: {
value: state.privateKey,
action: {
icon: state.privateKeyVisible ? EyeOff : Eye,
onClick: () =>
dispatch({ type: "TOGGLE_PRIVATE_KEY_VISIBILITY" }),
},
showCopyButton: true,
showPasswordToggle: true,
},
},
{
Expand All @@ -211,6 +208,7 @@ export const Security = () => {
"Sent out to other nodes on the mesh to allow them to compute a shared secret key",
properties: {
value: state.publicKey,
showCopyButton: true,
},
},
],
Expand Down Expand Up @@ -271,6 +269,7 @@ export const Security = () => {
],
properties: {
value: state.adminKey,
showCopyButton: true,
action: {
icon: state.adminKeyVisible ? EyeOff : Eye,
onClick: () =>
Expand Down
42 changes: 33 additions & 9 deletions src/components/UI/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";

import { cn } from "@core/utils/cn.ts";

const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 cursor-pointer",
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:opacity-50 dark:focus:ring-slate-400 disabled:cursor-not-allowed dark:focus:ring-offset-slate-900 cursor-pointer",
{
variants: {
variant: {
Expand All @@ -27,6 +26,7 @@ const buttonVariants = cva(
default: "h-10 py-2 px-4",
sm: "h-9 px-2 rounded-md",
lg: "h-11 px-8 rounded-md",
icon: "h-10 w-10",
},
},
defaultVariants: {
Expand All @@ -39,26 +39,50 @@ const buttonVariants = cva(
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];

export interface ButtonProps
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { }
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
icon?: React.ReactNode;
iconAlignment?: "left" | "right";
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, disabled, ...props }, ref) => {
(
{
className,
variant,
size,
disabled,
icon,
iconAlignment = "left",
children,
...props
},
ref,
) => {
return (
<button
type="button"
className={cn(
buttonVariants({ variant, size, className }),
{ "cursor-not-allowed": disabled }
{ "cursor-not-allowed": disabled },
"inline-flex items-center"
)}
ref={ref}
disabled={disabled}
{...props}
/>
>
{icon && iconAlignment === "left" && (
<span className={cn({ "mr-2": !!children })}>{icon}</span>
)}
{children}

{icon && iconAlignment === "right" && (
<span className={cn({ "ml-2": !!children })}>{icon}</span>
)}
</button>
);
},
);
Button.displayName = "Button";

export { Button, buttonVariants };
export { Button, buttonVariants };
2 changes: 1 addition & 1 deletion src/components/UI/Command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const CommandInput = React.forwardRef<
className="flex items-center border-b border-b-slate-100 px-4 dark:border-b-slate-700"
cmdk-input-wrapper=""
>
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50 text-white" />
<CommandPrimitive.Input
ref={ref}
className={cn(
Expand Down
37 changes: 19 additions & 18 deletions src/components/UI/Generator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,26 @@ import {
SelectTrigger,
SelectValue,
} from "@components/UI/Select.tsx";
import type { LucideIcon } from "lucide-react";

export interface ActionButton {
text: string;
onClick: React.MouseEventHandler<HTMLButtonElement>;
variant: ButtonVariant;
className?: string;
}[]

export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
type: "text" | "password";
devicePSKBitCount?: number;
value: string;
id: string;
variant: "default" | "invalid";
actionButtons: {
text: string;
onClick: React.MouseEventHandler<HTMLButtonElement>;
variant: ButtonVariant;
className?: string;
}[];
actionButtons: ActionButton[];
bits?: { text: string; value: string; key: string }[];
selectChange: (event: string) => void;
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
action?: {
icon: LucideIcon;
onClick: () => void;
};
inputChange: (event: React.ChangeEventHandler<HTMLInputElement> | undefined) => void;
showPasswordToggle?: boolean;
showCopyButton?: boolean;
disabled?: boolean;
}

Expand All @@ -50,8 +49,9 @@ const Generator =
],
selectChange,
inputChange,
action,
disabled,
showPasswordToggle,
showCopyButton,
...props
}: GeneratorProps
) => {
Expand All @@ -78,27 +78,28 @@ const Generator =
variant={variant}
value={value}
onChange={inputChange}
action={action}
disabled={disabled}
ref={inputRef}
showCopyButton={showCopyButton}
showPasswordToggle={showPasswordToggle}
/>
<Select
value={devicePSKBitCount?.toString()}
onValueChange={(e) => selectChange(e)}
disabled={disabled}
>
<SelectTrigger>
<SelectTrigger className="w-36 ml-2">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectContent className="w-36">
{bits.map(({ text, value, key }) => (
<SelectItem key={key} value={value}>
<SelectItem key={key} value={value} className="w-36">
{text}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex ml-4 space-x-4">
<div className="flex ml-2 space-x-2">
{actionButtons?.map(({ text, onClick, variant, className }) => (
<Button
key={text}
Expand Down
Loading