diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000000..c0d20008ea8 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. + +YES/NO + +## What kind of change does this PR introduce? + +Bug fix, feature, docs update, ... + +## What is the current behavior? + +Please link any relevant issues here. + +## What is the new behavior? + +Feel free to include screenshots if it includes visual changes. + +## Additional context + +Add any other context or screenshots. diff --git a/.vale.ini b/.vale.ini index 6b721717743..1ff9438668a 100644 --- a/.vale.ini +++ b/.vale.ini @@ -8,5 +8,7 @@ mdx = md [*] BasedOnStyles = Vale, Custom -BlockIgnores = (?s) *(.*?) -TokenIgnores = (href=".*?") +BlockIgnores = (?s) *(.*?), \ +(?s) *(export\s+const\s+\S+\s*=\s*\[.*?\]), \ +(?s) *() +TokenIgnores = (href\s*([=:])\s*['"].*?\1), \ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..fe66ea7fb32 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# CONTRIBUTING.md + +Thank you for contributing to Supabase! We’re a big, exciting open source project and we’d love to have you contribute! Here’s some resources and guidance to help you get started: + +[1. Getting Started](#getting-started) +[2. Issues](#issues) +[3. Pull Requests](#pull-requests) + +## Getting Started + +To ensure a positive and inclusive environment, please read our [code of conduct](https://github.com/supabase/.github/blob/main/CODE_OF_CONDUCT.md) before contributing. For help setting up the code in this repo, please follow our [DEVELOPERS.md](https://github.com/supabase/supabase/blob/master/DEVELOPERS.md) file. For the [docs](https://supabase.com/docs) site, follow this [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md) guide. + +## Issues + +If you find a bug, please create an Issue and we’ll triage it. + +- Please search [existing Issues](https://github.com/supabase/supabase) before creating a new one. +- Please include a clear description of the problem along with steps to reproduce it. Exact steps with screenshots and urls really help here. + +## Pull Requests + +We actively welcome your Pull Requests! A couple of things to keep in mind before you submit: + +- If you’re fixing an Issue, make sure someone else hasn’t already created a PR fixing the same issue. Likewise, make sure to link your PR to the related Issue(s). +- We will always try to accept the first viable PR that resolves the Issue. +- If you're new, we encourage you to take a look at issues tagged with [good first issue](https://github.com/supabase/supabase/pulls?q=is%3Aopen+is%3Apr+label%3A%22good+first+issue%22). +- If you’re submitting a new feature, make sure you have opened a [Discussion](https://github.com/orgs/supabase/discussions/new/choose) to discuss the new feature before opening a PR. We’d love to accept your hard work, but unfortunately if a feature hasn’t gone through a proper design process, your PR will be closed. +- Please use the PR message template and provide detailed context for quicker review. PRs without clear problem statements will be closed. + +Prior to submitting your PR, please conduct the following pre-flight checks: + +- Run `npm run build` locally to ensure that your code builds successfully without having to wait on us to approve Vercel Preview deploys. +- Ensure that the Prettier tests run successfully on your PR. + +Running these before you create the PR will help reduce back and forth with the team. diff --git a/apps/database-new/app/LoginDialog.tsx b/apps/database-new/app/LoginDialog.tsx index 10d6f2fd26e..21f14041d28 100644 --- a/apps/database-new/app/LoginDialog.tsx +++ b/apps/database-new/app/LoginDialog.tsx @@ -1,18 +1,18 @@ 'use client' import { useAppStateSnapshot } from '@/lib/state' -import { DialogContent_Shadcn_, Dialog_Shadcn_ } from 'ui' +import { DialogContent, Dialog } from 'ui' const LoginDialog = ({ children }: { children: React.ReactNode }) => { const snap = useAppStateSnapshot() return ( - snap.setLoginDialogOpen(!snap.loginDialogOpen)} > - {children} - + {children} + ) } diff --git a/apps/database-new/middleware.ts b/apps/database-new/middleware.ts new file mode 100644 index 00000000000..a1dd8cb4e34 --- /dev/null +++ b/apps/database-new/middleware.ts @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' + +// Redirect to supabase.com until this site is live +// when ready, remove the VERCEL env var at +// https://vercel.com/supabase/database-new/settings/environment-variables +export function middleware(request: NextRequest) { + if (process.env.VERCEL === '1') { + return NextResponse.redirect(new URL('https://supabase.com', request.url)) + } +} diff --git a/apps/docs/components/Favicons.tsx b/apps/docs/components/Favicons.tsx index 8ab82f3c178..cc7073cce92 100644 --- a/apps/docs/components/Favicons.tsx +++ b/apps/docs/components/Favicons.tsx @@ -42,7 +42,7 @@ const Favicons = () => { {/* misc */} - + diff --git a/apps/docs/components/Feedback/Feedback.tsx b/apps/docs/components/Feedback/Feedback.tsx new file mode 100644 index 00000000000..96137bfd4a1 --- /dev/null +++ b/apps/docs/components/Feedback/Feedback.tsx @@ -0,0 +1,204 @@ +import { useSupabaseClient } from '@supabase/auth-helpers-react' +import { Check, MessageSquareQuote, X } from 'lucide-react' +import { usePathname } from 'next/navigation' +import { + type CSSProperties, + type MouseEventHandler, + forwardRef, + useReducer, + useRef, + useState, +} from 'react' + +import { useIsLoggedIn } from 'common' +import { Button, cn } from 'ui' + +import { useSendFeedbackMutation } from '~/lib/fetch/feedback' +import { useSendTelemetryEvent } from '~/lib/telemetry' +import { FeedbackModal, type FeedbackFields } from './FeedbackModal' + +const FeedbackButton = forwardRef< + HTMLButtonElement, + { isYes: boolean; onClick: MouseEventHandler; visible: boolean } +>(({ isYes, onClick, visible }, ref) => { + const isLoggedIn = useIsLoggedIn() + if (!isLoggedIn) return null + + return ( + + ) +}) +FeedbackButton.displayName = 'FeedbackButton' + +type Response = 'yes' | 'no' + +enum StateType { + Unanswered = 'unanswered', + Followup = 'followup', +} + +type State = { type: StateType.Unanswered } | { type: StateType.Followup; response: Response } + +type Action = { event: 'VOTED'; response: Response } + +const initialState = { type: StateType.Unanswered } satisfies State + +function reducer(state: State, action: Action) { + switch (action.event) { + case 'VOTED': + if (state.type === StateType.Unanswered) + return { type: StateType.Followup, response: action.response } + default: + return state + } +} + +function Feedback() { + const [state, dispatch] = useReducer(reducer, initialState) + const [modalOpen, setModalOpen] = useState(false) + const feedbackButtonRef = useRef(null) + + const pathname = usePathname() + const sendTelemetryEvent = useSendTelemetryEvent() + const { mutate: sendFeedbackComment } = useSendFeedbackMutation() + const supabase = useSupabaseClient() + + const unanswered = state.type === 'unanswered' + const isYes = 'response' in state && state.response === 'yes' + const isNo = 'response' in state && state.response === 'no' + const showYes = unanswered || isYes + const showNo = unanswered || isNo + + async function sendFeedbackVote(response: Response) { + const { error } = await supabase.from('feedback').insert({ vote: response, page: pathname }) + if (error) console.error(error) + } + + function handleVote(response: Response) { + sendTelemetryEvent({ + category: 'docs', + action: 'feedback_voted', + label: response, + }) + sendFeedbackVote(response) + dispatch({ event: 'VOTED', response }) + // Focus so screen reader users are aware of the new element + setTimeout(() => { + feedbackButtonRef.current?.focus() + // Wait for element to show up first + }, 700) + } + + function refocusButton() { + setTimeout(() => { + feedbackButtonRef.current?.focus() + // Wait for modal to disappear and button to become focusable again + }, 100) + } + + async function handleSubmit({ page, comment }: FeedbackFields) { + sendFeedbackComment({ message: comment, pathname: page }) + setModalOpen(false) + refocusButton() + } + + return ( +
+

+ Is this helpful? +

+
+
+ + +
+
+ Thanks for your feedback! + {/** + setModalOpen(true)} + isYes={isYes} + visible={!unanswered} + /> + */} +
+
+ { + setModalOpen(false) + refocusButton() + }} + onSubmit={handleSubmit} + /> +
+ ) +} + +export { Feedback } diff --git a/apps/docs/components/Feedback/FeedbackModal.tsx b/apps/docs/components/Feedback/FeedbackModal.tsx new file mode 100644 index 00000000000..6013eb972d1 --- /dev/null +++ b/apps/docs/components/Feedback/FeedbackModal.tsx @@ -0,0 +1,65 @@ +import { Button, Form, Input, Modal } from 'ui' + +export type FeedbackFields = { + page: string + comment: string +} + +type FeedbackModalProps = { + visible: boolean + page: string + onCancel: () => void + onSubmit: (values: FeedbackFields) => void +} + +function FeedbackModal({ visible, page, onCancel, onSubmit }: FeedbackModalProps) { + return ( + +
{ + const errors: Partial = {} + + if (!vals.comment) { + errors.comment = 'Required' + } + + return errors + }} + onReset={onCancel} + onSubmit={onSubmit} + > + {({ isSubmitting }: { isSubmitting: boolean }) => ( + <> + + + + + + +
+ + +
+
+ + )} + +
+ ) +} + +export { FeedbackModal } diff --git a/apps/docs/components/Feedback/index.tsx b/apps/docs/components/Feedback/index.tsx new file mode 100644 index 00000000000..4772c0be963 --- /dev/null +++ b/apps/docs/components/Feedback/index.tsx @@ -0,0 +1 @@ +export { Feedback } from './Feedback' diff --git a/apps/docs/components/MDX/storage_management.mdx b/apps/docs/components/MDX/storage_management.mdx deleted file mode 100644 index 167fa3691d8..00000000000 --- a/apps/docs/components/MDX/storage_management.mdx +++ /dev/null @@ -1,111 +0,0 @@ -If you upload additional profile photos, they'll accumulate -in the `avatars` bucket because of their random names with only the latest being referenced -from `public.profiles` and the older versions getting orphaned. - -To automatically remove obsolete storage objects, extend the database -triggers. Note that it is not sufficient to delete the objects from the -`storage.objects` table because that would orphan and leak the actual storage objects in -the S3 backend. Instead, invoke the storage API within Postgres via the `http` extension. - -Enable the [http extension for the `extensions` schema](https://supabase.com/dashboard/project/_/database/extensions) in the Dashboard. -Then, define the following SQL functions in the SQL Editor to delete -storage objects via the API: - -```sql -create or replace function delete_storage_object(bucket text, object text, out status int, out content text) -returns record -language 'plpgsql' -security definer -as $$ -declare - project_url text := ''; - service_role_key text := ''; -- full access needed - url text := project_url||'/storage/v1/object/'||bucket||'/'||object; -begin - select - into status, content - result.status::int, result.content::text - FROM extensions.http(( - 'DELETE', - url, - ARRAY[extensions.http_header('authorization','Bearer '||service_role_key)], - NULL, - NULL)::extensions.http_request) as result; -end; -$$; - -create or replace function delete_avatar(avatar_url text, out status int, out content text) -returns record -language 'plpgsql' -security definer -as $$ -begin - select - into status, content - result.status, result.content - from public.delete_storage_object('avatars', avatar_url) as result; -end; -$$; - -``` - -Next, add a trigger that removes any obsolete avatar whenever the -profile is updated or deleted: - -```sql -create or replace function delete_old_avatar() -returns trigger -language 'plpgsql' -security definer -as $$ -declare - status int; - content text; - avatar_name text; -begin - if coalesce(old.avatar_url, '') <> '' - and (tg_op = 'DELETE' or (old.avatar_url <> coalesce(new.avatar_url, ''))) then - -- extract avatar name - avatar_name := old.avatar_url; - select - into status, content - result.status, result.content - from public.delete_avatar(avatar_name) as result; - if status <> 200 then - raise warning 'Could not delete avatar: % %', status, content; - end if; - end if; - if tg_op = 'DELETE' then - return old; - end if; - return new; -end; -$$; - -create trigger before_profile_changes - before update of avatar_url or delete on public.profiles - for each row execute function public.delete_old_avatar(); - -``` - -Finally, delete the `public.profile` row before a user is deleted. -If this step is omitted, you won't be able to delete users without -first manually deleting their avatar image. - -```sql -create or replace function delete_old_profile() -returns trigger -language 'plpgsql' -security definer -as $$ -begin - delete from public.profiles where id = old.id; - return old; -end; -$$; - -create trigger before_delete_user - before delete on auth.users - for each row execute function public.delete_old_profile(); - -``` diff --git a/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx index 924899c732d..944345923cf 100644 --- a/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx @@ -40,11 +40,7 @@ const NavigationMenuHome = () => { > {link?.icon && } {link.label} - {link.community && ( - - Community - - )} + {link.community && Community} ) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index b533e6779b9..e0380f96950 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -264,7 +264,7 @@ export const gettingstarted: NavMenuConstant = { ], }, { - name: 'Web app tutorials', + name: 'Web app demos', items: [ { name: 'Next.js', @@ -492,6 +492,12 @@ export const PhoneLoginsItems = [ isDarkMode: true, hasLightIcon: true, }, + { + name: 'Textlocal SMS Login (Community Supported)', + icon: '/docs/img/icons/textlocal-icon', + url: '/guides/auth/phone-login/textlocal', + linkDescription: 'Textlocal is a cloud-based SMS platform offering bulk messaging services.', + }, ] export const auth = { @@ -607,6 +613,10 @@ export const auth = { name: 'Column Level Security', url: '/guides/auth/column-level-security', }, + { + name: 'Custom Claims & RBAC', + url: '/guides/auth/custom-claims-and-role-based-access-control-rbac', + }, ], }, { @@ -690,6 +700,12 @@ export const database: NavMenuConstant = { { name: 'Connecting to your database', url: '/guides/database/connecting-to-postgres', + items: [ + { + name: 'Serverless Drivers', + url: '/guides/database/connecting-to-postgres/serverless-drivers', + }, + ], }, { name: 'Importing data', url: '/guides/database/import-data' }, { name: 'Securing your data', url: '/guides/database/secure-data' }, @@ -1118,6 +1134,10 @@ export const functions: NavMenuConstant = { name: 'Debugging Edge Functions', url: '/guides/functions/debugging', }, + { + name: 'Logging and Using Headers', + url: '/guides/functions/functions-headers', + }, { name: 'Testing your Edge Functions', url: '/guides/functions/unit-test', @@ -1187,6 +1207,10 @@ export const functions: NavMenuConstant = { name: 'Hugging Face', url: '/guides/ai/examples/huggingface-image-captioning', }, + { + name: 'Monitoring with Sentry', + url: '/guides/functions/examples/sentry-monitoring', + }, { name: 'OpenAI API', url: '/guides/ai/examples/openai' }, { name: 'Sending Emails with Resend', @@ -1387,6 +1411,7 @@ export const ai = { url: '/guides/ai/vector-indexes', items: vectorIndexItems, }, + { name: 'Hybrid search', url: '/guides/ai/hybrid-search' }, { name: 'Engineering for scale', url: '/guides/ai/engineering-for-scale', @@ -1579,6 +1604,15 @@ export const platform: NavMenuConstant = { { name: 'Read Replicas', url: '/guides/platform/read-replicas' }, ], }, + { + name: 'Logging and observability', + url: undefined, + items: [ + { name: 'Logging', url: '/guides/platform/logs' }, + { name: 'Metrics', url: '/guides/platform/metrics' }, + { name: 'Monitoring with Sentry', url: '/guides/platform/sentry-monitoring' }, + ], + }, { name: 'Platform Management', url: undefined, @@ -1594,8 +1628,6 @@ export const platform: NavMenuConstant = { name: 'HTTP Status Codes', url: '/guides/platform/http-status-codes', }, - { name: 'Logging', url: '/guides/platform/logs' }, - { name: 'Metrics', url: '/guides/platform/metrics' }, { name: 'Migrating and Upgrading', url: '/guides/platform/migrating-and-upgrading-projects', diff --git a/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx index 5ac5557f0d9..ad1a9bdec4a 100644 --- a/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx @@ -2,7 +2,8 @@ import { useTheme } from 'next-themes' import Image from 'next/legacy/image' import Link from 'next/link' import { FC } from 'react' -import { Button, IconCommand, IconGitHub, IconSearch, SearchButton } from 'ui' +import { Button, IconCommand, IconGitHub, IconSearch } from 'ui' +import { SearchButton } from 'ui-patterns/Cmdk' import { useIsLoggedIn, useIsUserLoading } from 'common' import { ThemeToggle } from 'ui-patterns/ThemeToggle' @@ -14,7 +15,7 @@ const TopNavBar: FC = () => { return (