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 && (
- } onClick={() => onSelectUrlToDelete(url)}>
- Remove
-
- )}
-
- )
- })
+ {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`}
/>
- }>
-
- Documentation
-
-
- 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})
+
+ >
+ ) : (
+ <>
+ }>
+
+ Documentation
+
+
+ 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'