diff --git a/apps/design-system/content/docs/icons.mdx b/apps/design-system/content/docs/icons.mdx index 7dc1dbf5a67..e0d0925e237 100644 --- a/apps/design-system/content/docs/icons.mdx +++ b/apps/design-system/content/docs/icons.mdx @@ -55,6 +55,7 @@ Follow these steps to add a new custom icon to the Supabase icon library. - Exported at 24x24px with `viewBox="0 0 24 24"` - Uses `stroke="currentColor"` for strokes (no hardcoded colors) + - Uses `stroke-width="1.5"` unless there is a good reason to deviate - Uses `fill="none"` for fills (no hardcoded colors) - Icon content is optically centered and around 18x18px within the 24x24 frame - Any unnecessary elements like ``, ``, and `` wrappers have been removed diff --git a/apps/design-system/content/docs/ui-patterns/empty-states.mdx b/apps/design-system/content/docs/ui-patterns/empty-states.mdx index 028746e62c0..709a72fba93 100644 --- a/apps/design-system/content/docs/ui-patterns/empty-states.mdx +++ b/apps/design-system/content/docs/ui-patterns/empty-states.mdx @@ -5,36 +5,12 @@ description: Convey the absence of data and provide clear instruction for what t Empty states convey the fact that there is nothing to list, perform, or display on the current page. **Ideally**, they also provide a clear action for the user to take. -## Missing route - -Users may accidentally navigate to a non-existent dynamic route, such as a non-existent bucket in [Storage](https://supabase.com/dashboard/project/_/storage) or a non-existent table in the [Table Editor](https://supabase.com/dashboard/project/_/editor). In these cases, follow the pattern of a centered [Admonition](../fragments/admonition) as shown below. - - - ## No data There are two ways an empty state may be displayed in cases where there is no data: -- **Zero results**: no data after a search or filter - **Initial state**: no data to begin with - -### Zero results - -Data-heavy presentations without results should have an empty state that broadly matches the state when there is data. This makes the transition between the two states more seamless. - -#### Table - -A [Table](../components/table) instance with zero results should display a single row. Dulling the TableHead text color and removing the TableCell hover state can further reinforce the lack of usable data. - - - -#### DataGrid - -[DataGrid](../components/data-table) typically spans the full height and width of a container. A classic example is [Users](https://supabase.com/dashboard/project/_/auth/users), which (as it sounds) displays a list of the project’s registered users. Any instance with zero results should display a more prominent empty with a clear title, description, and supporting illustration. - - - -Other DataGrid instances include [Cron Jobs](https://supabase.com/dashboard/project/_/integrations/cron/jobs) and [Queues](https://supabase.com/dashboard/project/_/integrations/queues). +- **Zero results**: no data after a search or filter ### Initial state @@ -56,6 +32,30 @@ Or perhaps the list type is data-heavy or does not benefit from additional infor Keep in mind that empty states will likely appear after a visual loading state. Consider layout shift and button placement during and after the transition. +### Zero results + +Data-heavy presentations without results should have an empty state that broadly matches the state when there is data. This makes the transition between the two states more seamless. + +#### Table + +A [Table](../components/table) instance with zero results should display a single row. Dulling the TableHead text color and removing the TableCell hover state can further reinforce the lack of usable data. + + + +#### DataGrid + +[DataGrid](../components/data-table) typically spans the full height and width of a container. A classic example is [Users](https://supabase.com/dashboard/project/_/auth/users), which (as it sounds) displays a list of the project’s registered users. Any instance with zero results should display a more prominent empty with a clear title, description, and supporting illustration. + + + +Other DataGrid instances include [Cron Jobs](https://supabase.com/dashboard/project/_/integrations/cron/jobs) and [Queues](https://supabase.com/dashboard/project/_/integrations/queues). + +## Missing route + +Users may accidentally navigate to a non-existent dynamic route, such as a non-existent bucket in [Storage](https://supabase.com/dashboard/project/_/storage) or a non-existent table in the [Table Editor](https://supabase.com/dashboard/project/_/editor). In these cases, follow the pattern of a centered [Admonition](../fragments/admonition) as shown below. + + + ## Components For presentational empty states (initial states with value propositions and actions), use the [EmptyStatePresentational](../fragments/empty-state-presentational) component from `ui-patterns`. This component provides a consistent structure with support for icons, titles, descriptions, and action buttons. diff --git a/apps/studio/components/interfaces/Home/ProjectList/EmptyStates.tsx b/apps/studio/components/interfaces/Home/ProjectList/EmptyStates.tsx index f2dabecd8bb..e7db6d16c1a 100644 --- a/apps/studio/components/interfaces/Home/ProjectList/EmptyStates.tsx +++ b/apps/studio/components/interfaces/Home/ProjectList/EmptyStates.tsx @@ -1,3 +1,4 @@ +import { BoxPlus } from 'icons' import { Plus } from 'lucide-react' import Link from 'next/link' @@ -15,6 +16,7 @@ import { TableHeader, TableRow, } from 'ui' +import { EmptyStatePresentational } from 'ui-patterns' import { ShimmeringCard } from './ShimmeringCard' export const Header = () => { @@ -126,35 +128,29 @@ export const NoProjectsState = ({ slug }: { slug: string }) => { const projectCreationEnabled = useIsFeatureEnabled('projects:create') return ( -
-
-

No projects

-

Get started by creating a new project.

-
- + {projectCreationEnabled && ( - )} -
+ ) } -export const NoOrganizationsState = () => { +export const NoOrganizationsState = ({}) => { return ( -
-
-

You are not part of any organizations yet

-

- Create your first organization to get started with Supabase -

-
-
- -
-
+ + + ) } diff --git a/apps/studio/components/interfaces/Organization/OrgNotFound.tsx b/apps/studio/components/interfaces/Organization/OrgNotFound.tsx index 490a0bdc932..cf91ba5177c 100644 --- a/apps/studio/components/interfaces/Organization/OrgNotFound.tsx +++ b/apps/studio/components/interfaces/Organization/OrgNotFound.tsx @@ -1,4 +1,3 @@ -import AlertError from 'components/ui/AlertError' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { Skeleton } from 'ui' import { Admonition } from 'ui-patterns/admonition' @@ -16,20 +15,26 @@ export const OrgNotFound = ({ slug }: { slug?: string }) => { return ( <> {slug !== '_' && ( - - The selected organization does not exist or you don't have permission to access it.{' '} - {slug ? ( + - Contact the owner or administrator to create a new project in the {slug}{' '} - organization. + {slug ? ( + <> + The organization {slug}{' '} + + ) : ( + <>This organization + )} + does not exist or you do not have permission to access to it. Contact the the owner if + you believe this is a mistake. - ) : ( - <>Contact the owner or administrator to create a new project. - )} - + } + /> )} -

Select an organization to create your new project in

+

Select a different organization to create your new project in

{isOrganizationsLoading && ( @@ -40,7 +45,11 @@ export const OrgNotFound = ({ slug }: { slug?: string }) => { )} {isOrganizationsError && ( - + )} {isOrganizationsSuccess && organizations?.map((org) => ( diff --git a/apps/studio/components/interfaces/SignIn/LastSignInWrapper.tsx b/apps/studio/components/interfaces/SignIn/LastSignInWrapper.tsx index c0c1b1bedc1..122bc7bf15f 100644 --- a/apps/studio/components/interfaces/SignIn/LastSignInWrapper.tsx +++ b/apps/studio/components/interfaces/SignIn/LastSignInWrapper.tsx @@ -17,7 +17,7 @@ export function LastSignInWrapper({ {lastSignIn === type && ( Last used diff --git a/apps/studio/pages/organizations.tsx b/apps/studio/pages/organizations.tsx index 1a1421b1e69..3e892a721ef 100644 --- a/apps/studio/pages/organizations.tsx +++ b/apps/studio/pages/organizations.tsx @@ -4,6 +4,7 @@ import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { useParams } from 'common' +import { NoOrganizationsState } from 'components/interfaces/Home/ProjectList/EmptyStates' import { OrganizationCard } from 'components/interfaces/Organization/OrganizationCard' import AppLayout from 'components/layouts/AppLayout/AppLayout' import DefaultLayout from 'components/layouts/DefaultLayout' @@ -15,14 +16,8 @@ import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { withAuth } from 'hooks/misc/withAuth' import type { NextPageWithLayout } from 'types' -import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, - Button, - CriticalIcon, - Skeleton, -} from 'ui' +import { Button, Skeleton } from 'ui' +import { Admonition } from 'ui-patterns/admonition' import { Input } from 'ui-patterns/DataInputs/Input' const OrganizationsPage: NextPageWithLayout = () => { @@ -53,25 +48,21 @@ const OrganizationsPage: NextPageWithLayout = () => { {orgNotFound && ( - - - Organization not found - - That organization ({orgSlug}) does not exist or you don't have access to - it. - - - If you think this is an error, please reach out to the org owner to get access. - - + + The organization {orgSlug} does not exist + or you do not have permission to access to it. Contact the the owner if you believe + this is a mistake. + + } + /> )} - {organizations.length === 0 && orgNotFound && ( -

You don't have any organizations yet. Create one to get started.

- )} - -
- {organizations.length > 0 && ( + {organizations.length > 0 && ( +
{ value={search} onChange={(event) => setSearch(event.target.value)} /> - )} - {organizationCreationEnabled && ( - - )} -
+ {organizationCreationEnabled && ( + + )} +
+ )} + + {isSuccess && organizations.length === 0 && !isError && } {search.length > 0 && filteredOrganizations.length === 0 && ( diff --git a/packages/icons/__registry__/index.tsx b/packages/icons/__registry__/index.tsx index 44bebd2bc0b..c1b2e4a65a9 100644 --- a/packages/icons/__registry__/index.tsx +++ b/packages/icons/__registry__/index.tsx @@ -54,6 +54,16 @@ export const Index: Record = [ svg: "\n \n", jsx: "import { Auth } from \"icons\"\n \n " }, +{ + name: "box-plus", + componentName: "BoxPlus", + deprecated: false, + raw: "import createSupabaseIcon from '../createSupabaseIcon';\n\n/**\n * @component @name BoxPlus\n * @description Supabase SVG icon component, renders SVG Element with children.\n *\n * @preview ![img](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMjAuNzMxNSA3LjAwMTE5QzIwLjU1NiA2LjY5NzU0IDIwLjMwMzcgNi40NDUzOSAyMCA2LjI3MDAyTDEzIDIuMjcwMDJDMTIuNjk2IDIuMDk0NDkgMTIuMzUxMSAyLjAwMjA4IDEyIDIuMDAyMDhDMTEuNjQ4OSAyLjAwMjA4IDExLjMwNCAyLjA5NDQ5IDExIDIuMjcwMDJMNCA2LjI3MDAyQzMuNjk2MjYgNi40NDUzOSAzLjQ0Mzk4IDYuNjk3NTQgMy4yNjg0NiA3LjAwMTE5QzMuMDkyOTQgNy4zMDQ4MyAzLjAwMDM2IDcuNjQ5MyAzIDguMDAwMDJWMTZDMy4wMDAzNiAxNi4zNTA4IDMuMDkyOTQgMTYuNjk1MiAzLjI2ODQ2IDE2Ljk5ODlDMy40NDM5OCAxNy4zMDI1IDMuNjk2MjYgMTcuNTU0NyA0IDE3LjczTDExIDIxLjczQzExLjMwNCAyMS45MDU2IDExLjY0ODkgMjEuOTk4IDEyIDIxLjk5OCIgc3Ryb2tlPSIjMDAwIiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjogI2ZmZjsgYm9yZGVyLXJhZGl1czogMnB4IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0zLjMgN0wxMiAxMkwyMC43IDciIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0xMiAyMlYxMiIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZD0iTTE5IDE0VjIwIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMTYgMTdIMjIiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=)\n *\n * @param {Object} props - Supabase icons props and any valid SVG attribute\n * @returns {JSX.Element} JSX Element\n *\n */\nconst BoxPlus = createSupabaseIcon('BoxPlus', [\n [\n 'path',\n {\n d: 'M20.7315 7.00119C20.556 6.69754 20.3037 6.44539 20 6.27002L13 2.27002C12.696 2.09449 12.3511 2.00208 12 2.00208C11.6489 2.00208 11.304 2.09449 11 2.27002L4 6.27002C3.69626 6.44539 3.44398 6.69754 3.26846 7.00119C3.09294 7.30483 3.00036 7.6493 3 8.00002V16C3.00036 16.3508 3.09294 16.6952 3.26846 16.9989C3.44398 17.3025 3.69626 17.5547 4 17.73L11 21.73C11.304 21.9056 11.6489 21.998 12 21.998',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: '1g22fp',\n },\n ],\n [\n 'path',\n {\n d: 'M3.3 7L12 12L20.7 7',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: '1v4gtz',\n },\n ],\n [\n 'path',\n {\n d: 'M12 22V12',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: 'hoijqc',\n },\n ],\n [\n 'path',\n {\n d: 'M19 14V20',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: '7ts4zv',\n },\n ],\n [\n 'path',\n {\n d: 'M16 17H22',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: 'ir88vv',\n },\n ],\n]);\n\nexport default BoxPlus;\n", + component: React.lazy(() => import('icons/src/icons/box-plus')), + import: "import { BoxPlus } from 'icons'", + svg: "\n\n\n\n\n\n\n", + jsx: "import { BoxPlus } from \"icons\"\n \n " +}, { name: "bucket-plus", componentName: "BucketPlus", diff --git a/packages/icons/src/icons/box-plus.ts b/packages/icons/src/icons/box-plus.ts new file mode 100644 index 00000000000..754e1589662 --- /dev/null +++ b/packages/icons/src/icons/box-plus.ts @@ -0,0 +1,71 @@ +import createSupabaseIcon from '../createSupabaseIcon'; + +/** + * @component @name BoxPlus + * @description Supabase SVG icon component, renders SVG Element with children. + * + * @preview ![img](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMjAuNzMxNSA3LjAwMTE5QzIwLjU1NiA2LjY5NzU0IDIwLjMwMzcgNi40NDUzOSAyMCA2LjI3MDAyTDEzIDIuMjcwMDJDMTIuNjk2IDIuMDk0NDkgMTIuMzUxMSAyLjAwMjA4IDEyIDIuMDAyMDhDMTEuNjQ4OSAyLjAwMjA4IDExLjMwNCAyLjA5NDQ5IDExIDIuMjcwMDJMNCA2LjI3MDAyQzMuNjk2MjYgNi40NDUzOSAzLjQ0Mzk4IDYuNjk3NTQgMy4yNjg0NiA3LjAwMTE5QzMuMDkyOTQgNy4zMDQ4MyAzLjAwMDM2IDcuNjQ5MyAzIDguMDAwMDJWMTZDMy4wMDAzNiAxNi4zNTA4IDMuMDkyOTQgMTYuNjk1MiAzLjI2ODQ2IDE2Ljk5ODlDMy40NDM5OCAxNy4zMDI1IDMuNjk2MjYgMTcuNTU0NyA0IDE3LjczTDExIDIxLjczQzExLjMwNCAyMS45MDU2IDExLjY0ODkgMjEuOTk4IDEyIDIxLjk5OCIgc3Ryb2tlPSIjMDAwIiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjogI2ZmZjsgYm9yZGVyLXJhZGl1czogMnB4IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0zLjMgN0wxMiAxMkwyMC43IDciIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0xMiAyMlYxMiIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZD0iTTE5IDE0VjIwIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMTYgMTdIMjIiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=) + * + * @param {Object} props - Supabase icons props and any valid SVG attribute + * @returns {JSX.Element} JSX Element + * + */ +const BoxPlus = createSupabaseIcon('BoxPlus', [ + [ + 'path', + { + d: 'M20.7315 7.00119C20.556 6.69754 20.3037 6.44539 20 6.27002L13 2.27002C12.696 2.09449 12.3511 2.00208 12 2.00208C11.6489 2.00208 11.304 2.09449 11 2.27002L4 6.27002C3.69626 6.44539 3.44398 6.69754 3.26846 7.00119C3.09294 7.30483 3.00036 7.6493 3 8.00002V16C3.00036 16.3508 3.09294 16.6952 3.26846 16.9989C3.44398 17.3025 3.69626 17.5547 4 17.73L11 21.73C11.304 21.9056 11.6489 21.998 12 21.998', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + key: '1g22fp', + }, + ], + [ + 'path', + { + d: 'M3.3 7L12 12L20.7 7', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + key: '1v4gtz', + }, + ], + [ + 'path', + { + d: 'M12 22V12', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + key: 'hoijqc', + }, + ], + [ + 'path', + { + d: 'M19 14V20', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + key: '7ts4zv', + }, + ], + [ + 'path', + { + d: 'M16 17H22', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + key: 'ir88vv', + }, + ], +]); + +export default BoxPlus; diff --git a/packages/icons/src/icons/index.ts b/packages/icons/src/icons/index.ts index 856b975cc2b..f3485bdccfd 100644 --- a/packages/icons/src/icons/index.ts +++ b/packages/icons/src/icons/index.ts @@ -3,6 +3,7 @@ export { default as ExampleTemplate } from './_example-template'; export { default as AnalyticsBucket } from './analytics-bucket'; export { default as ApiDocs } from './api-docs'; export { default as Auth } from './auth'; +export { default as BoxPlus } from './box-plus'; export { default as BucketPlus } from './bucket-plus'; export { default as Database } from './database'; export { default as Datadog } from './datadog'; diff --git a/packages/icons/src/raw-icons/box-plus.svg b/packages/icons/src/raw-icons/box-plus.svg new file mode 100644 index 00000000000..d5fa81e2cfc --- /dev/null +++ b/packages/icons/src/raw-icons/box-plus.svg @@ -0,0 +1,7 @@ + + + + + + +