Compare commits

...

5 Commits

Author SHA1 Message Date
Scott Wilson
6905ffba4e improvement: handle overflow and improve ui 2024-11-29 13:43:06 -08:00
Scott Wilson
64fd423c61 improvement: update import secret env select 2024-11-29 13:34:36 -08:00
Scott Wilson
da1a7466d1 improvement: change label 2024-11-29 13:28:53 -08:00
Scott Wilson
d3f3f34129 improvement: update copy secrets from env select and secret selection 2024-11-29 13:27:24 -08:00
Scott Wilson
c8fba7ce4c improvement: align pagination left on grid view project overview 2024-11-29 11:17:54 -08:00
5 changed files with 93 additions and 133 deletions

View File

@@ -58,6 +58,7 @@ export const FilterableSelect = <T,>({
clearIndicator: () => "p-1 hover:text-red text-bunker-400", clearIndicator: () => "p-1 hover:text-red text-bunker-400",
indicatorSeparator: () => "bg-bunker-400", indicatorSeparator: () => "bg-bunker-400",
dropdownIndicator: () => "text-bunker-200 p-1", dropdownIndicator: () => "text-bunker-200 p-1",
menuList: () => "flex flex-col gap-1",
menu: () => menu: () =>
"mt-2 p-2 border text-sm text-mineshaft-200 thin-scrollbar bg-mineshaft-900 border-mineshaft-600 rounded-md", "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", groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
@@ -65,7 +66,7 @@ export const FilterableSelect = <T,>({
twMerge( twMerge(
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600", isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
isSelected && "text-mineshaft-200", 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" noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
}} }}

View File

@@ -54,7 +54,7 @@ export const Pagination = ({
)} )}
> >
{startAdornment} {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"> <div className="text-xs">
{(page - 1) * perPage + 1} - {Math.min((page - 1) * perPage + perPage, count)} of {count} {(page - 1) * perPage + 1} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
</div> </div>

View File

@@ -876,7 +876,7 @@ const OrganizationPage = () => {
<Pagination <Pagination
className={ className={
projectsViewMode === ProjectsViewMode.GRID 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" : "rounded-b-md border border-mineshaft-600"
} }
perPage={perPage} perPage={perPage}

View File

@@ -6,6 +6,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
FilterableSelect,
FormControl, FormControl,
Modal, Modal,
ModalContent, ModalContent,
@@ -17,7 +18,7 @@ import { useSubscription, useWorkspace } from "@app/context";
import { useCreateSecretImport } from "@app/hooks/api"; import { useCreateSecretImport } from "@app/hooks/api";
const typeSchema = z.object({ const typeSchema = z.object({
environment: z.string().trim(), environment: z.object({ name: z.string(), slug: z.string() }),
secretPath: z secretPath: z
.string() .string()
.trim() .trim()
@@ -80,7 +81,7 @@ export const CreateSecretImportForm = ({
path: secretPath, path: secretPath,
isReplication, isReplication,
import: { import: {
environment: importedEnv, environment: importedEnv.slug,
path: importedSecPath path: importedSecPath
} }
}); });
@@ -88,8 +89,9 @@ export const CreateSecretImportForm = ({
reset(); reset();
createNotification({ createNotification({
type: "success", 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) { } catch (err) {
console.error(err); console.error(err);
@@ -111,6 +113,7 @@ export const CreateSecretImportForm = ({
return ( return (
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}> <Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
<ModalContent <ModalContent
bodyClassName="overflow-visible"
title="Add Secret Link" title="Add Secret Link"
subTitle="To inherit secrets from another environment or folder" subTitle="To inherit secrets from another environment or folder"
> >
@@ -118,21 +121,16 @@ export const CreateSecretImportForm = ({
<Controller <Controller
control={control} control={control}
name="environment" name="environment"
defaultValue={environments?.[0]?.slug} render={({ field: { onChange, value }, fieldState: { error } }) => (
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Environment" errorText={error?.message} isError={Boolean(error)}> <FormControl label="Environment" errorText={error?.message} isError={Boolean(error)}>
<Select <FilterableSelect
defaultValue={field.value} options={environments}
{...field} getOptionLabel={(option) => option.name}
onValueChange={(e) => onChange(e)} getOptionValue={(option) => option.slug}
className="w-full" placeholder="Select environment..."
> value={value}
{environments.map(({ name, slug }) => ( onChange={onChange}
<SelectItem value={slug} key={slug}> />
{name}
</SelectItem>
))}
</Select>
</FormControl> </FormControl>
)} )}
/> />
@@ -142,7 +140,7 @@ export const CreateSecretImportForm = ({
defaultValue="/" defaultValue="/"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}> <FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
<SecretPathInput {...field} environment={selectedEnvironment} /> <SecretPathInput {...field} environment={selectedEnvironment?.slug} />
</FormControl> </FormControl>
)} )}
/> />

View File

@@ -1,14 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { import { faClone, faFileImport, faSquareCheck } from "@fortawesome/free-solid-svg-icons";
faClone,
faFileImport,
faKey,
faSearch,
faSquareCheck,
faSquareXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
@@ -16,17 +9,13 @@ import { z } from "zod";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
Checkbox, FilterableSelect,
EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input,
Modal, Modal,
ModalContent, ModalContent,
ModalTrigger, ModalTrigger,
Select, Switch,
SelectItem,
Skeleton,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { SecretPathInput } from "@app/components/v2/SecretPathInput";
@@ -35,14 +24,17 @@ import { useDebounce } from "@app/hooks";
import { useGetProjectSecrets } from "@app/hooks/api"; import { useGetProjectSecrets } from "@app/hooks/api";
const formSchema = z.object({ const formSchema = z.object({
environment: z.string().trim(), environment: z.object({ name: z.string(), slug: z.string() }),
secretPath: z secretPath: z
.string() .string()
.trim() .trim()
.transform((val) => .transform((val) =>
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : 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>; type TFormSchema = z.infer<typeof formSchema>;
@@ -68,7 +60,6 @@ export const CopySecretsFromBoard = ({
onToggle, onToggle,
onParsedEnv onParsedEnv
}: Props) => { }: Props) => {
const [searchFilter, setSearchFilter] = useState("");
const [shouldIncludeValues, setShouldIncludeValues] = useState(true); const [shouldIncludeValues, setShouldIncludeValues] = useState(true);
const { const {
@@ -80,7 +71,7 @@ export const CopySecretsFromBoard = ({
formState: { isDirty } formState: { isDirty }
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug } defaultValues: { secretPath: "/", environment: environments?.[0] }
}); });
const envCopySecPath = watch("secretPath"); const envCopySecPath = watch("secretPath");
@@ -89,7 +80,7 @@ export const CopySecretsFromBoard = ({
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId, workspaceId,
environment: selectedEnvSlug, environment: selectedEnvSlug.slug,
secretPath: debouncedEnvCopySecretPath, secretPath: debouncedEnvCopySecretPath,
options: { options: {
enabled: enabled:
@@ -101,29 +92,22 @@ export const CopySecretsFromBoard = ({
}); });
useEffect(() => { useEffect(() => {
setValue("secrets", {}); setValue("secrets", []);
setSearchFilter(""); }, [debouncedEnvCopySecretPath, selectedEnvSlug]);
}, [debouncedEnvCopySecretPath]);
const handleSecSelectAll = () => { const handleSecSelectAll = () => {
if (secrets) { if (secrets) {
setValue( setValue("secrets", secrets, { shouldDirty: true });
"secrets",
secrets?.reduce((prev, curr) => ({ ...prev, [curr.key]: curr.value }), {}),
{ shouldDirty: true }
);
} }
}; };
const handleFormSubmit = async (data: TFormSchema) => { const handleFormSubmit = async (data: TFormSchema) => {
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {}; const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
Object.keys(data.secrets || {}).forEach((key) => { data.secrets.forEach(({ key, value }) => {
if (data.secrets[key]) { secretsToBePulled[key] = {
secretsToBePulled[key] = { value: (shouldIncludeValues && value) || "",
value: (shouldIncludeValues && data.secrets[key]) || "", comments: [""]
comments: [""] };
};
}
}); });
onParsedEnv(secretsToBePulled); onParsedEnv(secretsToBePulled);
onToggle(false); onToggle(false);
@@ -136,7 +120,6 @@ export const CopySecretsFromBoard = ({
onOpenChange={(state) => { onOpenChange={(state) => {
onToggle(state); onToggle(state);
reset(); reset();
setSearchFilter("");
}} }}
> >
<ModalTrigger asChild> <ModalTrigger asChild>
@@ -165,6 +148,7 @@ export const CopySecretsFromBoard = ({
</div> </div>
</ModalTrigger> </ModalTrigger>
<ModalContent <ModalContent
bodyClassName="overflow-visible"
className="max-w-2xl" className="max-w-2xl"
title="Copy Secret From An Environment" title="Copy Secret From An Environment"
subTitle="Copy/paste secrets from other environments into this context" subTitle="Copy/paste secrets from other environments into this context"
@@ -176,22 +160,14 @@ export const CopySecretsFromBoard = ({
name="environment" name="environment"
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<FormControl label="Environment" isRequired className="w-1/3"> <FormControl label="Environment" isRequired className="w-1/3">
<Select <FilterableSelect
value={value} value={value}
onValueChange={(val) => onChange(val)} onChange={onChange}
className="w-full border border-mineshaft-500" options={environments}
defaultValue={environments?.[0]?.slug} placeholder="Select environment..."
position="popper" getOptionLabel={(option) => option.name}
> getOptionValue={(option) => option.slug}
{environments.map((sourceEnvironment) => ( />
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl> </FormControl>
)} )}
/> />
@@ -203,7 +179,7 @@ export const CopySecretsFromBoard = ({
<SecretPathInput <SecretPathInput
{...field} {...field}
placeholder="Provide a path, default is /" placeholder="Provide a path, default is /"
environment={selectedEnvSlug} environment={selectedEnvSlug?.slug}
/> />
</FormControl> </FormControl>
)} )}
@@ -212,72 +188,57 @@ export const CopySecretsFromBoard = ({
<div className="border-t border-mineshaft-600 pt-4"> <div className="border-t border-mineshaft-600 pt-4">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div>Secrets</div> <div>Secrets</div>
<div className="flex w-1/2 items-center space-x-2"> </div>
<Input <div className="flex w-full items-start gap-3">
placeholder="Search for secret" <Controller
value={searchFilter} 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" size="xs"
leftIcon={<FontAwesomeIcon icon={faSearch} />} onClick={handleSecSelectAll}
onChange={(evt) => setSearchFilter(evt.target.value)} >
/> <FontAwesomeIcon icon={faSquareCheck} size="lg" />
<Tooltip content="Select All"> </IconButton>
<IconButton </Tooltip>
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>
</div> </div>
{!isSecretsLoading && !secrets?.length && ( <div className="my-6 ml-2">
<EmptyState title="No secrets found" icon={faKey} /> <Switch
)}
<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
id="populate-include-value" id="populate-include-value"
isChecked={shouldIncludeValues} isChecked={shouldIncludeValues}
onCheckedChange={(isChecked) => setShouldIncludeValues(isChecked as boolean)} onCheckedChange={(isChecked) => setShouldIncludeValues(isChecked as boolean)}
> >
Include secret values Include secret values
</Checkbox> </Switch>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<Button <Button
@@ -285,7 +246,7 @@ export const CopySecretsFromBoard = ({
type="submit" type="submit"
isDisabled={!isDirty} isDisabled={!isDirty}
> >
Paste Secrets Copy Secrets
</Button> </Button>
<Button variant="plain" colorSchema="secondary" onClick={() => onToggle(false)}> <Button variant="plain" colorSchema="secondary" onClick={() => onToggle(false)}>
Cancel Cancel