Skip to content

Commit c8007f8

Browse files
Merge pull request #5611 from Infisical/SECRETS-34
feat(secrets-overview): re-vamp create secret form
2 parents 58a1da7 + 2fddd1b commit c8007f8

File tree

16 files changed

+1428
-213
lines changed

16 files changed

+1428
-213
lines changed

frontend/src/components/v3/generic/Field/Field.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>)
9595
<Label
9696
data-slot="field-label"
9797
className={cn(
98-
"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",
98+
"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",
9999
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
100100
className
101101
)}
Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
1+
import { forwardRef } from "react";
2+
13
import { cn } from "../../utils";
24

3-
function UnstableInput({ className, type, ...props }: React.ComponentProps<"input">) {
5+
const UnstableInput = forwardRef<
6+
HTMLInputElement,
7+
React.ComponentProps<"input"> & { isError?: boolean }
8+
>(({ className, type, isError, ...props }, ref) => {
49
return (
510
<input
11+
ref={ref}
612
type={type}
713
data-slot="input"
814
className={cn(
915
"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",
1016
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
11-
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
17+
"aria-invalid:border-danger aria-invalid:ring-danger/40",
1218
"selection:bg-foreground selection:text-background",
1319
className
1420
)}
21+
aria-invalid={isError}
1522
{...props}
1623
/>
1724
);
18-
}
25+
});
26+
27+
UnstableInput.displayName = "UnstableInput";
1928

2029
export { UnstableInput };
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { useMemo, useState } from "react";
2+
import { CheckIcon, CopyIcon, KeyRoundIcon, RefreshCwIcon } from "lucide-react";
3+
4+
import { useTimedReset } from "@app/hooks";
5+
6+
import { Button } from "../Button";
7+
import { Checkbox } from "../Checkbox";
8+
import { UnstableIconButton } from "../IconButton";
9+
import { Label } from "../Label";
10+
import { Popover, PopoverContent, PopoverTrigger } from "../Popover";
11+
12+
type PasswordOptionsType = {
13+
length: number;
14+
useUppercase: boolean;
15+
useLowercase: boolean;
16+
useNumbers: boolean;
17+
useSpecialChars: boolean;
18+
};
19+
20+
export type PasswordGeneratorProps = {
21+
onUsePassword?: (password: string) => void;
22+
isDisabled?: boolean;
23+
minLength?: number;
24+
maxLength?: number;
25+
};
26+
27+
export const PasswordGenerator = ({
28+
onUsePassword,
29+
isDisabled = false,
30+
minLength = 12,
31+
maxLength = 64
32+
}: PasswordGeneratorProps) => {
33+
const [isOpen, setIsOpen] = useState(false);
34+
const [, isCopying, setCopyText] = useTimedReset<string>({
35+
initialState: "Copy"
36+
});
37+
const [refresh, setRefresh] = useState(false);
38+
const [passwordOptions, setPasswordOptions] = useState<PasswordOptionsType>({
39+
length: minLength,
40+
useUppercase: true,
41+
useLowercase: true,
42+
useNumbers: true,
43+
useSpecialChars: true
44+
});
45+
46+
const password = useMemo(() => {
47+
const charset = {
48+
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
49+
lowercase: "abcdefghijklmnopqrstuvwxyz",
50+
numbers: "0123456789",
51+
specialChars: "-_.~!*"
52+
};
53+
54+
let availableChars = "";
55+
if (passwordOptions.useUppercase) availableChars += charset.uppercase;
56+
if (passwordOptions.useLowercase) availableChars += charset.lowercase;
57+
if (passwordOptions.useNumbers) availableChars += charset.numbers;
58+
if (passwordOptions.useSpecialChars) availableChars += charset.specialChars;
59+
60+
if (availableChars === "") availableChars = charset.lowercase + charset.numbers;
61+
62+
const randomBytes = new Uint32Array(passwordOptions.length);
63+
crypto.getRandomValues(randomBytes);
64+
let newPassword = "";
65+
for (let i = 0; i < passwordOptions.length; i += 1) {
66+
newPassword += availableChars[randomBytes[i] % availableChars.length];
67+
}
68+
69+
return newPassword;
70+
// eslint-disable-next-line react-hooks/exhaustive-deps
71+
}, [passwordOptions, refresh]);
72+
73+
const copyToClipboard = () => {
74+
navigator.clipboard
75+
.writeText(password)
76+
.then(() => {
77+
setCopyText("Copied");
78+
})
79+
.catch(() => {
80+
setCopyText("Copy failed");
81+
});
82+
};
83+
84+
const usePassword = () => {
85+
onUsePassword?.(password);
86+
setIsOpen(false);
87+
};
88+
89+
return (
90+
<Popover open={isOpen} onOpenChange={setIsOpen}>
91+
<PopoverTrigger asChild>
92+
<UnstableIconButton variant="outline" size="md" isDisabled={isDisabled}>
93+
<KeyRoundIcon />
94+
</UnstableIconButton>
95+
</PopoverTrigger>
96+
<PopoverContent className="w-[30rem]" align="end">
97+
<div className="flex flex-col gap-4">
98+
<div>
99+
<p className="text-sm font-medium">Generate Random Value</p>
100+
<p className="mt-0.5 text-xs text-muted">Generate strong unique values</p>
101+
</div>
102+
103+
<div className="rounded-md border border-border bg-container/50 p-3">
104+
<div className="flex items-center justify-between gap-2">
105+
<p className="flex-1 font-mono text-sm break-all select-all">{password}</p>
106+
<div className="flex shrink-0 gap-1">
107+
<UnstableIconButton
108+
variant="ghost"
109+
size="xs"
110+
onClick={() => setRefresh((prev) => !prev)}
111+
>
112+
<RefreshCwIcon />
113+
</UnstableIconButton>
114+
<UnstableIconButton variant="ghost" size="xs" onClick={copyToClipboard}>
115+
{isCopying ? <CheckIcon /> : <CopyIcon />}
116+
</UnstableIconButton>
117+
</div>
118+
</div>
119+
</div>
120+
121+
<div>
122+
<div className="mb-2 flex items-center justify-between">
123+
<Label className="text-xs text-accent">Length: {passwordOptions.length}</Label>
124+
</div>
125+
<input
126+
type="range"
127+
min={minLength}
128+
max={maxLength}
129+
value={passwordOptions.length}
130+
onChange={(e) =>
131+
setPasswordOptions({ ...passwordOptions, length: Number(e.target.value) })
132+
}
133+
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"
134+
/>
135+
</div>
136+
137+
<div className="flex flex-wrap gap-6">
138+
<Label className="flex cursor-pointer items-center gap-1.5 text-xs">
139+
<Checkbox
140+
variant="project"
141+
isChecked={passwordOptions.useUppercase}
142+
onCheckedChange={(checked) =>
143+
setPasswordOptions({ ...passwordOptions, useUppercase: checked as boolean })
144+
}
145+
/>
146+
A-Z
147+
</Label>
148+
<Label className="flex cursor-pointer items-center gap-1.5 text-xs">
149+
<Checkbox
150+
variant="project"
151+
isChecked={passwordOptions.useLowercase}
152+
onCheckedChange={(checked) =>
153+
setPasswordOptions({ ...passwordOptions, useLowercase: checked as boolean })
154+
}
155+
/>
156+
a-z
157+
</Label>
158+
<Label className="flex cursor-pointer items-center gap-1.5 text-xs">
159+
<Checkbox
160+
variant="project"
161+
isChecked={passwordOptions.useNumbers}
162+
onCheckedChange={(checked) =>
163+
setPasswordOptions({ ...passwordOptions, useNumbers: checked as boolean })
164+
}
165+
/>
166+
0-9
167+
</Label>
168+
<Label className="flex cursor-pointer items-center gap-1.5 text-xs">
169+
<Checkbox
170+
variant="project"
171+
isChecked={passwordOptions.useSpecialChars}
172+
onCheckedChange={(checked) =>
173+
setPasswordOptions({ ...passwordOptions, useSpecialChars: checked as boolean })
174+
}
175+
/>
176+
-_.~!*
177+
</Label>
178+
</div>
179+
180+
{onUsePassword && (
181+
<Button variant="project" size="xs" onClick={usePassword} className="w-full">
182+
Use Value
183+
</Button>
184+
)}
185+
</div>
186+
</PopoverContent>
187+
</Popover>
188+
);
189+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { PasswordGenerator, type PasswordGeneratorProps } from "./PasswordGenerator";

frontend/src/components/v3/generic/ReactSelect/CreatableSelect.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { GroupBase } from "react-select";
22
import ReactSelectCreatable, { CreatableProps } from "react-select/creatable";
33

44
import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "./components";
5-
import { selectClassNames, selectStyles } from "./styles";
5+
import { getSelectClassNames, selectClassNames, selectStyles } from "./styles";
66

77
export const CreatableSelect = <T,>({
88
isMulti,
99
closeMenuOnSelect,
10+
isError,
1011
...props
11-
}: CreatableProps<T, boolean, GroupBase<T>>) => {
12+
}: CreatableProps<T, boolean, GroupBase<T>> & { isError?: boolean }) => {
1213
return (
1314
<ReactSelectCreatable
1415
isMulti={isMulti}
@@ -18,7 +19,7 @@ export const CreatableSelect = <T,>({
1819
data-slot="creatable-select"
1920
styles={selectStyles as any}
2021
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
21-
classNames={selectClassNames as any}
22+
classNames={(isError ? getSelectClassNames(isError) : selectClassNames) as any}
2223
{...props}
2324
/>
2425
);

frontend/src/components/v3/generic/ReactSelect/FilterableSelect.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Select, { GroupBase, Props } from "react-select";
22

33
import { ClearIndicator, DropdownIndicator, Group, MultiValueRemove, Option } from "./components";
4-
import { selectClassNames, selectStyles } from "./styles";
4+
import { getSelectClassNames, selectClassNames, selectStyles } from "./styles";
55

66
export const FilterableSelect = <T,>({
77
isMulti,
@@ -10,10 +10,12 @@ export const FilterableSelect = <T,>({
1010
groupBy = null,
1111
getGroupHeaderLabel = null,
1212
options = [],
13+
isError,
1314
...props
1415
}: Props<T, boolean, GroupBase<T>> & {
1516
groupBy?: string | null;
1617
getGroupHeaderLabel?: ((groupValue: any) => string) | null;
18+
isError?: boolean;
1719
}) => {
1820
let processedOptions: Props<T, boolean, GroupBase<T>>["options"] = options;
1921

@@ -56,7 +58,7 @@ export const FilterableSelect = <T,>({
5658
Group,
5759
...props.components
5860
}}
59-
classNames={selectClassNames as any}
61+
classNames={(isError ? getSelectClassNames(isError) : selectClassNames) as any}
6062
{...props}
6163
/>
6264
);

frontend/src/components/v3/generic/ReactSelect/styles.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,21 @@ export const selectStyles: StylesConfig<unknown, boolean, GroupBase<unknown>> =
5050
zIndex: 99999
5151
})
5252
};
53+
54+
export const getSelectClassNames = (
55+
isError?: boolean
56+
): ClassNamesConfig<unknown, boolean, GroupBase<unknown>> => ({
57+
...selectClassNames,
58+
control: ({ isFocused }) =>
59+
cn(
60+
"!min-h-9 w-full cursor-pointer rounded-md border bg-transparent py-1 pr-1 pl-2 text-sm",
61+
// eslint-disable-next-line no-nested-ternary
62+
isError
63+
? isFocused
64+
? "border-danger ring-[3px] ring-danger/40"
65+
: "border-danger"
66+
: isFocused
67+
? "border-ring ring-[3px] ring-ring/50"
68+
: "border-border hover:border-foreground/20"
69+
)
70+
});

0 commit comments

Comments
 (0)