From 3e2f8cf51b8d4d1f64ec6ef7ea1aec94719c5942 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Fri, 20 Sep 2024 02:47:10 -0700 Subject: [PATCH] feat: allow removing multiple redirect urls (#29405) * feat: allow removing multiple redirect urls * fix: add checkboxes to redirect url rows * fix: close modal on success & change bg color on hover * fix: replace remove button with checkbox * Some changes --------- Co-authored-by: Joshen Lim --- .../Auth/RedirectUrls/RedirectUrlList.tsx | 92 +++++++---- .../Auth/RedirectUrls/RedirectUrls.tsx | 148 ++++++++++++------ .../Auth/RedirectUrls/ValueContainer.tsx | 34 ++-- .../project/[ref]/auth/url-configuration.tsx | 2 +- 4 files changed, 183 insertions(+), 93 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx b/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx index 938b838df17..6e66f48cea1 100644 --- a/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx +++ b/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrlList.tsx @@ -1,41 +1,77 @@ import { noop } from 'lodash' -import { Button } from 'ui' +import { Globe } from 'lucide-react' import { EmptyListState } from 'components/ui/States' -import ValueContainer from './ValueContainer' -import { Globe, Trash } from 'lucide-react' +import { Checkbox_Shadcn_ } from 'ui' +import { ValueContainer } from './ValueContainer' interface RedirectUrlListProps { - URI_ALLOW_LIST_ARRAY: string[] + allowList: string[] canUpdate: boolean - onSelectUrlToDelete: (url: string) => void + selectedUrls: string[] + onSelectUrl: (urls: string[]) => void } -const RedirectUrlList = ({ - URI_ALLOW_LIST_ARRAY, - canUpdate, - onSelectUrlToDelete = noop, +export const RedirectUrlList = ({ + allowList, + selectedUrls, + onSelectUrl = noop, }: RedirectUrlListProps) => { + // [Joshen] One for next time: maybe shift this into a reusable logic since it + // seems like we can use this in multiple places for future + const onClickUrl = (event: any, url: string) => { + if (event.shiftKey) { + const urlIdx = allowList.indexOf(url) + const idxLatest = allowList.indexOf(selectedUrls[selectedUrls.length - 1]) + + const newSelectedUrls = + urlIdx > idxLatest + ? allowList.slice(idxLatest + 1, urlIdx + 1) + : allowList.slice(urlIdx, idxLatest) + + const urlsNotSelectedYet = newSelectedUrls.filter((x) => !selectedUrls.includes(x)) + + if (urlsNotSelectedYet.length > 0) { + onSelectUrl([ + ...selectedUrls, + ...(urlIdx > idxLatest ? newSelectedUrls : newSelectedUrls.reverse()), + ]) + } else { + const urlsToRemove = newSelectedUrls.concat([selectedUrls[selectedUrls.length - 1]]) + onSelectUrl(selectedUrls.filter((x) => !urlsToRemove.includes(x))) + } + } else { + const isSelected = selectedUrls.includes(url) + const newSelectedUrls = isSelected + ? selectedUrls.filter((selectedUrl) => selectedUrl !== url) + : [...selectedUrls, url] + onSelectUrl(newSelectedUrls) + } + } + return (
- {URI_ALLOW_LIST_ARRAY.length > 0 ? ( - URI_ALLOW_LIST_ARRAY.map((url) => { - return ( - -
- - - - {url} -
- {canUpdate && ( - - )} -
- ) - }) + {allowList.length > 0 ? ( + <> + {allowList.map((url) => { + const isSelected = selectedUrls.includes(url) + return ( + onClickUrl(e, url)}> +
+
+ + + +
+ {url} +
+ onClickUrl(e, url)} /> +
+
+
+ ) + })} + ) : (
) } - -export default RedirectUrlList diff --git a/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx b/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx index de9f7208475..1a59c673269 100644 --- a/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx +++ b/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx @@ -1,11 +1,12 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' -import { ExternalLink } from 'lucide-react' +import { ExternalLink, Trash } from 'lucide-react' import Link from 'next/link' import { useMemo, useState } from 'react' import { toast } from 'sonner' import { object, string } from 'yup' +import { Label } from '@ui/components/shadcn/ui/label' +import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FormHeader } from 'components/ui/Forms/FormHeader' import { HorizontalShimmerWithIcon } from 'components/ui/Shimmers/Shimmers' @@ -19,15 +20,17 @@ import { Button, DialogSectionSeparator, Form, - Input, Modal, WarningIcon, } from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' import { urlRegex } from '../Auth.constants' -import RedirectUrlList from './RedirectUrlList' -import ValueContainer from './ValueContainer' +import { RedirectUrlList } from './RedirectUrlList' +import { ValueContainer } from './ValueContainer' -const RedirectUrls = () => { +const MAX_URLS_LENGTH = 2 * 1024 + +export const RedirectUrls = () => { const { ref: projectRef } = useParams() const { data: authConfig, @@ -45,7 +48,8 @@ const RedirectUrls = () => { }, [authConfig?.URI_ALLOW_LIST]) const [open, setOpen] = useState(false) - const [selectedUrlToDelete, setSelectedUrlToDelete] = useState() + const [openRemoveSelected, setOpenRemoveSelected] = useState(false) + const [selectedUrls, setSelectedUrls] = useState([]) const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') @@ -64,7 +68,7 @@ const RedirectUrls = () => { const payloadString = payload.toString() - if (payloadString.length > 2 * 1024) { + if (payloadString.length > MAX_URLS_LENGTH) { return toast.error('Too many redirect URLs, please remove some or try to use wildcards') } @@ -82,21 +86,25 @@ const RedirectUrls = () => { ) } - const onConfirmDeleteUrl = async (url?: string) => { - if (!url) return + const onConfirmDeleteUrl = async (urls?: string[]) => { + if (!urls || urls.length === 0) return // Remove selectedUrl from array and update - const payload = URI_ALLOW_LIST_ARRAY.filter((e: string) => e !== url) - + const payload = URI_ALLOW_LIST_ARRAY.filter((url: string) => !selectedUrls.includes(url)) + const payloadString = payload.join(',') + if (payloadString.length > MAX_URLS_LENGTH) { + return toast.error('Too many redirect URLs, please remove some or try to use wildcards') + } updateAuthConfig( - { projectRef: projectRef!, config: { URI_ALLOW_LIST: payload.toString() } }, + { projectRef: projectRef!, config: { URI_ALLOW_LIST: payloadString } }, { onError: (error) => { - toast.error(`Failed to remove URL: ${error?.message}`) + toast.error(`Failed to remove URL(s): ${error?.message}`) }, onSuccess: () => { - setSelectedUrlToDelete(undefined) - toast.success('Successfully removed URL') + setSelectedUrls([]) + setOpenRemoveSelected(false) + toast.success('Successfully removed URL(s)') }, } ) @@ -110,27 +118,51 @@ const RedirectUrls = () => { description={`URLs that auth providers are permitted to redirect to post authentication. Wildcards are allowed, for example, https://*.domain.com`} />
- - setOpen(true)} - tooltip={{ - content: { - side: 'bottom', - text: 'You need additional permissions to update redirect URLs', - }, - }} - > - Add URL - + {selectedUrls.length > 0 ? ( + <> + + } + onClick={() => (selectedUrls.length > 0 ? setOpenRemoveSelected(true) : null)} + > + Remove ({selectedUrls.length}) + + + ) : ( + <> + + setOpen(true)} + tooltip={{ + content: { + side: 'bottom', + text: 'You need additional permissions to update redirect URLs', + }, + }} + > + Add URL + + + )}
{isLoading && ( @@ -152,9 +184,10 @@ const RedirectUrls = () => { )} {isSuccess && ( )} { {() => { return ( <> - - + + + @@ -198,18 +232,27 @@ const RedirectUrls = () => { setSelectedUrlToDelete(undefined)} + size="large" + visible={openRemoveSelected} + header="Remove URLs" + onCancel={() => { + setSelectedUrls([]) + setOpenRemoveSelected(false) + }} >

- Are you sure you want to remove{' '} - {selectedUrlToDelete}? + Are you sure you want to remove the following URLs?

+
    + {selectedUrls.map((url, index) => ( +
  • + {url} +
  • + ))} +

- This URL will no longer work with your authentication configuration. + These URLs will no longer work with your authentication configuration.

@@ -218,7 +261,10 @@ const RedirectUrls = () => { block type="default" size="medium" - onClick={() => setSelectedUrlToDelete(undefined)} + onClick={() => { + setSelectedUrls([]) + setOpenRemoveSelected(false) + }} > Cancel @@ -227,7 +273,7 @@ const RedirectUrls = () => { size="medium" type="warning" loading={isUpdatingConfig} - onClick={() => onConfirmDeleteUrl(selectedUrlToDelete)} + onClick={() => onConfirmDeleteUrl(selectedUrls)} > {isUpdatingConfig ? 'Removing...' : 'Remove URL'} @@ -236,5 +282,3 @@ const RedirectUrls = () => {
) } - -export default RedirectUrls diff --git a/apps/studio/components/interfaces/Auth/RedirectUrls/ValueContainer.tsx b/apps/studio/components/interfaces/Auth/RedirectUrls/ValueContainer.tsx index ec36304761c..c0f32b9a9f3 100644 --- a/apps/studio/components/interfaces/Auth/RedirectUrls/ValueContainer.tsx +++ b/apps/studio/components/interfaces/Auth/RedirectUrls/ValueContainer.tsx @@ -1,17 +1,29 @@ -import { PropsWithChildren } from 'react' +import { PropsWithChildren, MouseEvent } from 'react' -const ValueContainer = ({ children }: PropsWithChildren<{}>) => ( +import { cn } from 'ui' + +interface ValueContainerProps { + isSelected?: boolean + onClick?: (event: MouseEvent) => void +} + +export const ValueContainer = ({ + children, + isSelected = false, + onClick, +}: PropsWithChildren) => (
{ + if (onClick) onClick(e) + }} > {children}
) - -export default ValueContainer diff --git a/apps/studio/pages/project/[ref]/auth/url-configuration.tsx b/apps/studio/pages/project/[ref]/auth/url-configuration.tsx index 0d59eb7935c..36aae336e66 100644 --- a/apps/studio/pages/project/[ref]/auth/url-configuration.tsx +++ b/apps/studio/pages/project/[ref]/auth/url-configuration.tsx @@ -1,6 +1,6 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import RedirectUrls from 'components/interfaces/Auth/RedirectUrls/RedirectUrls' +import { RedirectUrls } from 'components/interfaces/Auth/RedirectUrls/RedirectUrls' import SiteUrl from 'components/interfaces/Auth/SiteUrl/SiteUrl' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import { FormsContainer } from 'components/ui/Forms/FormsContainer'