[7/n][project settings ui] AddEnvironmentVariableRowProps Input (#29)

This commit is contained in:
Vivian Phung 2024-05-14 16:00:30 -04:00 committed by GitHub
commit abbda18fc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 176 additions and 76 deletions

View File

@ -1,10 +1,9 @@
import { UseFormRegister } from 'react-hook-form'; import { UseFormRegister } from 'react-hook-form';
import { Input } from 'components/shared/Input';
import { EnvironmentVariablesFormValues } from '../../../../types'; import { EnvironmentVariablesFormValues } from '../../../../types';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { TrashIcon } from 'components/shared/CustomIcon'; import { TrashIcon } from 'components/shared/CustomIcon';
import { Input } from 'components/shared/Input';
interface AddEnvironmentVariableRowProps { interface AddEnvironmentVariableRowProps {
onDelete: () => void; onDelete: () => void;
@ -20,28 +19,24 @@ const AddEnvironmentVariableRow = ({
isDeleteDisabled, isDeleteDisabled,
}: AddEnvironmentVariableRowProps) => { }: AddEnvironmentVariableRowProps) => {
return ( return (
<div className="flex gap-1 p-2"> <div className="flex gap-1 py-4 self-stretch">
<div>
<Input <Input
label="key" size="md"
size="sm"
{...register(`variables.${index}.key`, { {...register(`variables.${index}.key`, {
required: 'Key field cannot be empty', required: 'Key field cannot be empty',
})} })}
label={index === 0 ? 'Key' : undefined}
/> />
</div>
<div>
<Input <Input
size="sm" size="md"
label="value" label={index === 0 ? 'Value' : undefined}
{...register(`variables.${index}.value`, { {...register(`variables.${index}.value`, {
required: 'Value field cannot be empty', required: 'Value field cannot be empty',
})} })}
/> />
</div>
<div className="self-end"> <div className="self-end">
<Button <Button
size="sm" size="md"
iconOnly iconOnly
onClick={() => onDelete()} onClick={() => onDelete()}
disabled={isDeleteDisabled} disabled={isDeleteDisabled}

View File

@ -1,7 +1,6 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { Project } from 'gql-client'; import { Project } from 'gql-client';
import { import {
@ -13,7 +12,9 @@ import {
Input, Input,
Typography, Typography,
} from '@snowballtools/material-tailwind-react-fork'; } from '@snowballtools/material-tailwind-react-fork';
import { useGQLClient } from '../../../../context/GQLClientContext'; import { useGQLClient } from '../../../../context/GQLClientContext';
import { useToast } from 'components/shared/Toast';
interface DeleteProjectDialogProp { interface DeleteProjectDialogProp {
open: boolean; open: boolean;
@ -26,6 +27,7 @@ const DeleteProjectDialog = ({
handleOpen, handleOpen,
project, project,
}: DeleteProjectDialogProp) => { }: DeleteProjectDialogProp) => {
const { toast, dismiss } = useToast();
const { orgSlug } = useParams(); const { orgSlug } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const client = useGQLClient(); const client = useGQLClient();
@ -46,7 +48,12 @@ const DeleteProjectDialog = ({
if (deleteProject) { if (deleteProject) {
navigate(`/${orgSlug}`); navigate(`/${orgSlug}`);
} else { } else {
toast.error('Project not deleted'); toast({
id: 'project_not_deleted',
title: 'Project not deleted',
variant: 'error',
onDismiss: dismiss,
});
} }
handleOpen(); handleOpen();

View File

@ -1,12 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { import { Collapse } from '@snowballtools/material-tailwind-react-fork';
Card,
Collapse,
Typography,
} from '@snowballtools/material-tailwind-react-fork';
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow'; import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
import { Environment, EnvironmentVariable } from 'gql-client'; import { Environment, EnvironmentVariable } from 'gql-client';
import {
ChevronDownSmallIcon,
ChevronUpSmallIcon,
} from 'components/shared/CustomIcon';
interface DisplayEnvironmentVariablesProps { interface DisplayEnvironmentVariablesProps {
environment: Environment; environment: Environment;
@ -27,20 +27,16 @@ const DisplayEnvironmentVariables = ({
className="flex gap-4 p-2" className="flex gap-4 p-2"
onClick={() => setOpenCollapse((cur) => !cur)} onClick={() => setOpenCollapse((cur) => !cur)}
> >
<div>^</div> {openCollapse ? <ChevronUpSmallIcon /> : <ChevronDownSmallIcon />}
<div className="grow capitalize">{environment}</div> <div className="grow capitalize">{environment}</div>
<div>{variables.length} variables</div> <div>{variables.length} variables</div>
</div> </div>
<Collapse open={openCollapse}> <Collapse open={openCollapse}>
{variables.length === 0 ? ( {variables.length === 0 ? (
<Card className="bg-gray-300 flex items-center p-4"> <div className="bg-slate-100 rounded-xl flex-col p-4">
<Typography variant="small" className="text-black"> No environment variables added yet. Once you add them, they'll show
No environment variables added yet. up here.
</Typography> </div>
<Typography variant="small">
Once you add them, theyll show up here.
</Typography>
</Card>
) : ( ) : (
variables.map((variable: EnvironmentVariable) => { variables.map((variable: EnvironmentVariable) => {
return ( return (

View File

@ -1,5 +1,4 @@
import { useState } from 'react'; import { useState } from 'react';
import toast from 'react-hot-toast';
import { Domain, DomainStatus, Project } from 'gql-client'; import { Domain, DomainStatus, Project } from 'gql-client';
import { import {
@ -15,6 +14,7 @@ import {
import EditDomainDialog from './EditDomainDialog'; import EditDomainDialog from './EditDomainDialog';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog'; import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog';
import { useToast } from 'components/shared/Toast';
enum RefreshStatus { enum RefreshStatus {
IDLE, IDLE,
@ -47,6 +47,7 @@ const DomainCard = ({
project, project,
onUpdate, onUpdate,
}: DomainCardProps) => { }: DomainCardProps) => {
const { toast, dismiss } = useToast();
const [refreshStatus, SetRefreshStatus] = useState(RefreshStatus.IDLE); const [refreshStatus, SetRefreshStatus] = useState(RefreshStatus.IDLE);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false);
@ -58,9 +59,19 @@ const DomainCard = ({
if (deleteDomain) { if (deleteDomain) {
onUpdate(); onUpdate();
toast.success(`Domain ${domain.name} deleted successfully`); toast({
id: 'domain_deleted_success',
title: 'Domain deleted',
variant: 'success',
onDismiss: dismiss,
});
} else { } else {
toast.error(`Error deleting domain ${domain.name}`); toast({
id: 'domain_deleted_error',
title: `Error deleting domain ${domain.name}`,
variant: 'error',
onDismiss: dismiss,
});
} }
}; };

View File

@ -5,12 +5,12 @@ import { EnvironmentVariable } from 'gql-client';
import { import {
IconButton, IconButton,
Input,
Typography, Typography,
} from '@snowballtools/material-tailwind-react-fork'; } from '@snowballtools/material-tailwind-react-fork';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { DeleteVariableDialog } from 'components/projects/Dialog/DeleteVariableDialog'; import { DeleteVariableDialog } from 'components/projects/Dialog/DeleteVariableDialog';
import { Input } from 'components/shared/Input';
const ShowPasswordIcon = ({ const ShowPasswordIcon = ({
handler, handler,
@ -96,7 +96,7 @@ const EditEnvironmentVariableRow = ({
<Input <Input
disabled={!edit} disabled={!edit}
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
icon={ leftIcon={
<ShowPasswordIcon <ShowPasswordIcon
handler={() => { handler={() => {
setShowPassword((prevShowPassword) => !prevShowPassword); setShowPassword((prevShowPassword) => !prevShowPassword);

View File

@ -1,4 +1,4 @@
import { GitSelect } from '../../../../types/types'; import { GitSelect } from '../../../../types';
const GitSelectionSection = ({ const GitSelectionSection = ({
gitSelectionHandler, gitSelectionHandler,

View File

@ -5,12 +5,12 @@ import {
Select, Select,
Option, Option,
Chip, Chip,
IconButton,
Tooltip,
} from '@snowballtools/material-tailwind-react-fork'; } from '@snowballtools/material-tailwind-react-fork';
import { formatAddress } from 'utils/format'; import { formatAddress } from 'utils/format';
import { RemoveMemberDialog } from 'components/projects/Dialog/RemoveMemberDialog'; import { RemoveMemberDialog } from 'components/projects/Dialog/RemoveMemberDialog';
import { Tooltip } from 'components/shared/Tooltip';
import { Button } from 'components/shared/Button';
const PERMISSION_OPTIONS = [ const PERMISSION_OPTIONS = [
{ {
@ -125,15 +125,15 @@ const MemberCard = ({
/> />
</div> </div>
<div> <div>
<IconButton <Button
size="sm" size="sm"
className="rounded-full" iconOnly
onClick={() => { onClick={() => {
setRemoveMemberDialogOpen((prevVal) => !prevVal); setRemoveMemberDialogOpen((prevVal) => !prevVal);
}} }}
> >
D D
</IconButton> </Button>
</div> </div>
</div> </div>
)} )}

View File

@ -1,9 +1,8 @@
import { useState } from 'react'; import { useState } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { Button } from '@snowballtools/material-tailwind-react-fork';
import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog'; import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog';
import { Button } from 'components/shared/Button';
interface WebhookCardProps { interface WebhookCardProps {
webhookUrl: string; webhookUrl: string;
@ -26,8 +25,8 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
C C
</Button> </Button>
<Button <Button
color="red"
size="sm" size="sm"
variant="danger"
onClick={() => { onClick={() => {
setDeleteDialogOpen(true); setDeleteDialogOpen(true);
}} }}

View File

@ -0,0 +1,20 @@
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ChevronDownSmallIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.4697 14.5303C11.7626 14.8232 12.2374 14.8232 12.5303 14.5303L16.5303 10.5303C16.8232 10.2374 16.8232 9.76256 16.5303 9.46967C16.2374 9.17678 15.7626 9.17678 15.4697 9.46967L12 12.9393L8.53033 9.46967C8.23744 9.17678 7.76256 9.17678 7.46967 9.46967C7.17678 9.76256 7.17678 10.2374 7.46967 10.5303L11.4697 14.5303Z"
fill="currentColor"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,20 @@
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ChevronUpSmallIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.4697 9.46967C11.7626 9.17678 12.2374 9.17678 12.5303 9.46967L16.5303 13.4697C16.8232 13.7626 16.8232 14.2374 16.5303 14.5303C16.2374 14.8232 15.7626 14.8232 15.4697 14.5303L12 11.0607L8.53033 14.5303C8.23744 14.8232 7.76256 14.8232 7.46967 14.5303C7.17678 14.2374 7.17678 13.7626 7.46967 13.4697L11.4697 9.46967Z"
fill="currentColor"
/>
</CustomIcon>
);
};

View File

@ -71,6 +71,8 @@ export * from './TrashIcon';
export * from './CopyUnfilledIcon'; export * from './CopyUnfilledIcon';
export * from './SwitchIcon'; export * from './SwitchIcon';
export * from './CollaboratorsIcon'; export * from './CollaboratorsIcon';
export * from './ChevronUpSmallIcon';
export * from './ChevronDownSmallIcon';
// Templates // Templates
export * from './templates'; export * from './templates';

View File

@ -4,20 +4,18 @@ import toast from 'react-hot-toast';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Environment, EnvironmentVariable } from 'gql-client'; import { Environment, EnvironmentVariable } from 'gql-client';
import { import { Collapse } from '@snowballtools/material-tailwind-react-fork';
Collapse,
Card,
Chip,
} from '@snowballtools/material-tailwind-react-fork';
import { useGQLClient } from '../../../../../context/GQLClientContext';
import { EnvironmentVariablesFormValues } from '../../../../../types';
import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow'; import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow';
import DisplayEnvironmentVariables from 'components/projects/project/settings/DisplayEnvironmentVariables'; import DisplayEnvironmentVariables from 'components/projects/project/settings/DisplayEnvironmentVariables';
import { useGQLClient } from '../../../../../context/GQLClientContext';
import { EnvironmentVariablesFormValues } from '../../../../../types';
import HorizontalLine from 'components/HorizontalLine'; import HorizontalLine from 'components/HorizontalLine';
import { Heading } from 'components/shared/Heading'; import { Heading } from 'components/shared/Heading';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { Checkbox } from 'components/shared/Checkbox'; import { Checkbox } from 'components/shared/Checkbox';
import { PlusIcon } from 'components/shared/CustomIcon';
import { InlineNotification } from 'components/shared/InlineNotification';
export const EnvironmentVariablesTabPanel = () => { export const EnvironmentVariablesTabPanel = () => {
const { id } = useParams(); const { id } = useParams();
@ -139,15 +137,18 @@ export const EnvironmentVariablesTabPanel = () => {
<p className="text-slate-600 text-sm font-normal leading-tight"> <p className="text-slate-600 text-sm font-normal leading-tight">
A new deployment is required for your changes to take effect. A new deployment is required for your changes to take effect.
</p> </p>
<div className="bg-gray-300 rounded-lg p-2"> <div className="bg-slate-100 rounded-xl flex-col">
<div <Heading
className="text-black"
onClick={() => setCreateNewVariable((cur) => !cur)} onClick={() => setCreateNewVariable((cur) => !cur)}
className="p-4"
> >
+ Create new variable <div className="flex gap-2 items-center">
<PlusIcon />
<span>Create new variable</span>
</div> </div>
</Heading>
<Collapse open={createNewVariable}> <Collapse open={createNewVariable}>
<Card className="bg-white p-2"> <div className="p-4 bg-slate-100">
<form onSubmit={handleSubmit(createEnvironmentVariablesHandler)}> <form onSubmit={handleSubmit(createEnvironmentVariablesHandler)}>
{fields.map((field, index) => { {fields.map((field, index) => {
return ( return (
@ -162,7 +163,7 @@ export const EnvironmentVariablesTabPanel = () => {
})} })}
<div className="flex gap-1 p-2"> <div className="flex gap-1 p-2">
<Button <Button
size="sm" size="md"
onClick={() => onClick={() =>
append({ append({
key: '', key: '',
@ -173,19 +174,18 @@ export const EnvironmentVariablesTabPanel = () => {
+ Add variable + Add variable
</Button> </Button>
{/* TODO: Implement import environment varible functionality */} {/* TODO: Implement import environment varible functionality */}
<Button size="sm" disabled> <Button size="md" disabled>
Import .env Import .env
</Button> </Button>
</div> </div>
{isFieldEmpty && ( {isFieldEmpty && (
<Chip <InlineNotification
value="^ Please ensure no fields are empty before saving." title="Please ensure no fields are empty before saving."
variant="outlined" variant="danger"
color="red" size="md"
size="sm"
/> />
)} )}
<div> <div className="flex gap-2 p-2">
<Checkbox <Checkbox
label="Production" label="Production"
{...register(`environment.production`)} {...register(`environment.production`)}
@ -208,7 +208,7 @@ export const EnvironmentVariablesTabPanel = () => {
</Button> </Button>
</div> </div>
</form> </form>
</Card> </div>
</Collapse> </Collapse>
</div> </div>
<div className="p-2"> <div className="p-2">

View File

@ -0,0 +1,25 @@
import { Meta, StoryObj } from '@storybook/react';
import { ChevronDownSmallIcon } from 'components/shared/CustomIcon';
const meta: Meta<typeof ChevronDownSmallIcon> = {
title: 'Icons/ChevronDownSmallIcon',
component: ChevronDownSmallIcon,
tags: ['autodocs'],
args: {
size: 'string | number' as unknown as any,
name: 'string',
},
};
export default meta;
type Story = StoryObj<typeof ChevronDownSmallIcon>;
export const Default: Story = {
render: ({ size, name }) => <ChevronDownSmallIcon size={size} name={name} />,
args: {
size: '24px',
name: 'plus',
},
};

View File

@ -1,6 +1,6 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { ChevronUpDown } from 'components/shared/CustomIcon/ChevronUpDown'; import { ChevronUpDown } from 'components/shared/CustomIcon';
const meta: Meta<typeof ChevronUpDown> = { const meta: Meta<typeof ChevronUpDown> = {
title: 'Icons/ChevronUpDown', title: 'Icons/ChevronUpDown',

View File

@ -0,0 +1,25 @@
import { Meta, StoryObj } from '@storybook/react';
import { ChevronUpSmallIcon } from 'components/shared/CustomIcon';
const meta: Meta<typeof ChevronUpSmallIcon> = {
title: 'Components/ChevronUpSmallIcon',
component: ChevronUpSmallIcon,
tags: ['autodocs'],
args: {
size: 'string | number' as unknown as any,
name: 'string',
},
};
export default meta;
type Story = StoryObj<typeof ChevronUpSmallIcon>;
export const Default: Story = {
render: ({ size, name }) => <ChevronUpSmallIcon size={size} name={name} />,
args: {
size: '24px',
name: 'chevron-up',
},
};