mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
Merge pull request #2824 from Infisical/environment-select-refactor
Improvement: Copy Secrets Modal & Environment Selects Improvements
This commit is contained in:
@ -58,6 +58,7 @@ export const FilterableSelect = <T,>({
|
||||
clearIndicator: () => "p-1 hover:text-red text-bunker-400",
|
||||
indicatorSeparator: () => "bg-bunker-400",
|
||||
dropdownIndicator: () => "text-bunker-200 p-1",
|
||||
menuList: () => "flex flex-col gap-1",
|
||||
menu: () =>
|
||||
"mt-2 p-2 border text-sm text-mineshaft-200 thin-scrollbar bg-mineshaft-900 border-mineshaft-600 rounded-md",
|
||||
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
|
||||
@ -65,7 +66,7 @@ export const FilterableSelect = <T,>({
|
||||
twMerge(
|
||||
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
|
||||
isSelected && "text-mineshaft-200",
|
||||
"hover:cursor-pointer mb-1 rounded text-xs px-3 py-2"
|
||||
"hover:cursor-pointer rounded text-xs px-3 py-2"
|
||||
),
|
||||
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
|
||||
}}
|
||||
|
@ -54,7 +54,7 @@ export const Pagination = ({
|
||||
)}
|
||||
>
|
||||
{startAdornment}
|
||||
<div className="ml-auto mr-6 flex items-center space-x-2">
|
||||
<div className={twMerge("mr-4 flex items-center space-x-2", startAdornment && "ml-auto")}>
|
||||
<div className="text-xs">
|
||||
{(page - 1) * perPage + 1} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
|
||||
</div>
|
||||
|
@ -876,7 +876,7 @@ const OrganizationPage = () => {
|
||||
<Pagination
|
||||
className={
|
||||
projectsViewMode === ProjectsViewMode.GRID
|
||||
? "col-span-full border-transparent bg-transparent"
|
||||
? "col-span-full !justify-start border-transparent bg-transparent pl-2"
|
||||
: "rounded-b-md border border-mineshaft-600"
|
||||
}
|
||||
perPage={perPage}
|
||||
|
@ -6,6 +6,7 @@ import { z } from "zod";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalContent,
|
||||
@ -17,7 +18,7 @@ import { useSubscription, useWorkspace } from "@app/context";
|
||||
import { useCreateSecretImport } from "@app/hooks/api";
|
||||
|
||||
const typeSchema = z.object({
|
||||
environment: z.string().trim(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
@ -80,7 +81,7 @@ export const CreateSecretImportForm = ({
|
||||
path: secretPath,
|
||||
isReplication,
|
||||
import: {
|
||||
environment: importedEnv,
|
||||
environment: importedEnv.slug,
|
||||
path: importedSecPath
|
||||
}
|
||||
});
|
||||
@ -88,8 +89,9 @@ export const CreateSecretImportForm = ({
|
||||
reset();
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: `Successfully linked. ${isReplication ? "Please refresh the dashboard to view changes" : ""
|
||||
}`
|
||||
text: `Successfully linked. ${
|
||||
isReplication ? "Please refresh the dashboard to view changes" : ""
|
||||
}`
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@ -111,6 +113,7 @@ export const CreateSecretImportForm = ({
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
|
||||
<ModalContent
|
||||
bodyClassName="overflow-visible"
|
||||
title="Add Secret Link"
|
||||
subTitle="To inherit secrets from another environment or folder"
|
||||
>
|
||||
@ -118,21 +121,16 @@ export const CreateSecretImportForm = ({
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl label="Environment" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{environments.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<FilterableSelect
|
||||
options={environments}
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
placeholder="Select environment..."
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@ -142,7 +140,7 @@ export const CreateSecretImportForm = ({
|
||||
defaultValue="/"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
|
||||
<SecretPathInput {...field} environment={selectedEnvironment} />
|
||||
<SecretPathInput {...field} environment={selectedEnvironment?.slug} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -1,14 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faClone,
|
||||
faFileImport,
|
||||
faKey,
|
||||
faSearch,
|
||||
faSquareCheck,
|
||||
faSquareXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faClone, faFileImport, faSquareCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
@ -16,17 +9,13 @@ import { z } from "zod";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
EmptyState,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalTrigger,
|
||||
Select,
|
||||
SelectItem,
|
||||
Skeleton,
|
||||
Switch,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
@ -35,14 +24,17 @@ import { useDebounce } from "@app/hooks";
|
||||
import { useGetProjectSecrets } from "@app/hooks/api";
|
||||
|
||||
const formSchema = z.object({
|
||||
environment: z.string().trim(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((val) =>
|
||||
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
|
||||
),
|
||||
secrets: z.record(z.string().optional().nullable())
|
||||
secrets: z
|
||||
.object({ key: z.string(), value: z.string().optional() })
|
||||
.array()
|
||||
.min(1, "Select one or more secrets to copy")
|
||||
});
|
||||
|
||||
type TFormSchema = z.infer<typeof formSchema>;
|
||||
@ -68,7 +60,6 @@ export const CopySecretsFromBoard = ({
|
||||
onToggle,
|
||||
onParsedEnv
|
||||
}: Props) => {
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const [shouldIncludeValues, setShouldIncludeValues] = useState(true);
|
||||
|
||||
const {
|
||||
@ -80,7 +71,7 @@ export const CopySecretsFromBoard = ({
|
||||
formState: { isDirty }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug }
|
||||
defaultValues: { secretPath: "/", environment: environments?.[0] }
|
||||
});
|
||||
|
||||
const envCopySecPath = watch("secretPath");
|
||||
@ -89,7 +80,7 @@ export const CopySecretsFromBoard = ({
|
||||
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||
workspaceId,
|
||||
environment: selectedEnvSlug,
|
||||
environment: selectedEnvSlug.slug,
|
||||
secretPath: debouncedEnvCopySecretPath,
|
||||
options: {
|
||||
enabled:
|
||||
@ -101,29 +92,22 @@ export const CopySecretsFromBoard = ({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setValue("secrets", {});
|
||||
setSearchFilter("");
|
||||
}, [debouncedEnvCopySecretPath]);
|
||||
setValue("secrets", []);
|
||||
}, [debouncedEnvCopySecretPath, selectedEnvSlug]);
|
||||
|
||||
const handleSecSelectAll = () => {
|
||||
if (secrets) {
|
||||
setValue(
|
||||
"secrets",
|
||||
secrets?.reduce((prev, curr) => ({ ...prev, [curr.key]: curr.value }), {}),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
setValue("secrets", secrets, { shouldDirty: true });
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: TFormSchema) => {
|
||||
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
|
||||
Object.keys(data.secrets || {}).forEach((key) => {
|
||||
if (data.secrets[key]) {
|
||||
secretsToBePulled[key] = {
|
||||
value: (shouldIncludeValues && data.secrets[key]) || "",
|
||||
comments: [""]
|
||||
};
|
||||
}
|
||||
data.secrets.forEach(({ key, value }) => {
|
||||
secretsToBePulled[key] = {
|
||||
value: (shouldIncludeValues && value) || "",
|
||||
comments: [""]
|
||||
};
|
||||
});
|
||||
onParsedEnv(secretsToBePulled);
|
||||
onToggle(false);
|
||||
@ -136,7 +120,6 @@ export const CopySecretsFromBoard = ({
|
||||
onOpenChange={(state) => {
|
||||
onToggle(state);
|
||||
reset();
|
||||
setSearchFilter("");
|
||||
}}
|
||||
>
|
||||
<ModalTrigger asChild>
|
||||
@ -165,6 +148,7 @@ export const CopySecretsFromBoard = ({
|
||||
</div>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
bodyClassName="overflow-visible"
|
||||
className="max-w-2xl"
|
||||
title="Copy Secret From An Environment"
|
||||
subTitle="Copy/paste secrets from other environments into this context"
|
||||
@ -176,22 +160,14 @@ export const CopySecretsFromBoard = ({
|
||||
name="environment"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl label="Environment" isRequired className="w-1/3">
|
||||
<Select
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
position="popper"
|
||||
>
|
||||
{environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
onChange={onChange}
|
||||
options={environments}
|
||||
placeholder="Select environment..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.slug}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@ -203,7 +179,7 @@ export const CopySecretsFromBoard = ({
|
||||
<SecretPathInput
|
||||
{...field}
|
||||
placeholder="Provide a path, default is /"
|
||||
environment={selectedEnvSlug}
|
||||
environment={selectedEnvSlug?.slug}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
@ -212,72 +188,57 @@ export const CopySecretsFromBoard = ({
|
||||
<div className="border-t border-mineshaft-600 pt-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>Secrets</div>
|
||||
<div className="flex w-1/2 items-center space-x-2">
|
||||
<Input
|
||||
placeholder="Search for secret"
|
||||
value={searchFilter}
|
||||
</div>
|
||||
<div className="flex w-full items-start gap-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name="secrets"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="flex-1"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<FilterableSelect
|
||||
placeholder={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
isSecretsLoading
|
||||
? "Loading secrets..."
|
||||
: secrets?.length
|
||||
? "Select secrets..."
|
||||
: "No secrets found..."
|
||||
}
|
||||
isLoading={isSecretsLoading}
|
||||
options={secrets}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isMulti
|
||||
getOptionValue={(option) => option.key}
|
||||
getOptionLabel={(option) => option.key}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Tooltip content="Select All">
|
||||
<IconButton
|
||||
className="mt-1 h-9 w-9"
|
||||
ariaLabel="Select all"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
||||
onChange={(evt) => setSearchFilter(evt.target.value)}
|
||||
/>
|
||||
<Tooltip content="Select All">
|
||||
<IconButton
|
||||
ariaLabel="Select all"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={handleSecSelectAll}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSquareCheck} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Unselect All">
|
||||
<IconButton
|
||||
ariaLabel="UnSelect all"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSquareXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
onClick={handleSecSelectAll}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSquareCheck} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{!isSecretsLoading && !secrets?.length && (
|
||||
<EmptyState title="No secrets found" icon={faKey} />
|
||||
)}
|
||||
<div className="thin-scrollbar grid max-h-64 grid-cols-2 gap-4 overflow-auto ">
|
||||
{isSecretsLoading &&
|
||||
Array.apply(0, Array(2)).map((_x, i) => (
|
||||
<Skeleton key={`secret-pull-loading-${i + 1}`} className="bg-mineshaft-700" />
|
||||
))}
|
||||
|
||||
{secrets
|
||||
?.filter(({ key }) => key.toLowerCase().includes(searchFilter.toLowerCase()))
|
||||
?.map(({ id, key, value: secVal }) => (
|
||||
<Controller
|
||||
key={`pull-secret--${id}`}
|
||||
control={control}
|
||||
name={`secrets.${key}`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox
|
||||
id={`pull-secret-${id}`}
|
||||
isChecked={Boolean(value)}
|
||||
onCheckedChange={(isChecked) => onChange(isChecked ? secVal : "")}
|
||||
>
|
||||
{key}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 mb-4">
|
||||
<Checkbox
|
||||
<div className="my-6 ml-2">
|
||||
<Switch
|
||||
id="populate-include-value"
|
||||
isChecked={shouldIncludeValues}
|
||||
onCheckedChange={(isChecked) => setShouldIncludeValues(isChecked as boolean)}
|
||||
>
|
||||
Include secret values
|
||||
</Checkbox>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
@ -285,7 +246,7 @@ export const CopySecretsFromBoard = ({
|
||||
type="submit"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
Paste Secrets
|
||||
Copy Secrets
|
||||
</Button>
|
||||
<Button variant="plain" colorSchema="secondary" onClick={() => onToggle(false)}>
|
||||
Cancel
|
||||
|
Reference in New Issue
Block a user