diff --git a/apps/design-system/README.md b/apps/design-system/README.md index 959e363180250..f7d0347f0b5a4 100644 --- a/apps/design-system/README.md +++ b/apps/design-system/README.md @@ -64,7 +64,7 @@ With that out of the way, there are several parts of this design system that nee - `registry/charts.ts`: chart components - `registry/fragments.ts`: fragment components -You may need to rebuild the design system’s registry. You can do that via: +You will probably need to rebuild the design system’s registry after making new additions. You can do that via: ```bash cd apps/design-system diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index cead6b766f8e2..2bb69907c83ec 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -38,6 +38,39 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "PageContainer": { + name: "PageContainer", + type: "components:fragment", + registryDependencies: undefined, + component: React.lazy(() => import("@/../../packages/ui-patterns/src/PageContainer")), + source: "", + files: ["registry/default//PageContainer/index.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "PageHeader": { + name: "PageHeader", + type: "components:fragment", + registryDependencies: undefined, + component: React.lazy(() => import("@/../../packages/ui-patterns/src/PageHeader")), + source: "", + files: ["registry/default//PageHeader/index.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "PageSection": { + name: "PageSection", + type: "components:fragment", + registryDependencies: undefined, + component: React.lazy(() => import("@/../../packages/ui-patterns/src/PageSection")), + source: "", + files: ["registry/default//PageSection/index.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "accordion-demo": { name: "accordion-demo", type: "components:example", @@ -1886,6 +1919,105 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "page-container-demo": { + name: "page-container-demo", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-container-demo")), + source: "", + files: ["registry/default/example/page-container-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-layout-detail": { + name: "page-layout-detail", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-layout-detail")), + source: "", + files: ["registry/default/example/page-layout-detail.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-layout-list": { + name: "page-layout-list", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-layout-list")), + source: "", + files: ["registry/default/example/page-layout-list.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-layout-list-simple": { + name: "page-layout-list-simple", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-layout-list-simple")), + source: "", + files: ["registry/default/example/page-layout-list-simple.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-layout-settings": { + name: "page-layout-settings", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-layout-settings")), + source: "", + files: ["registry/default/example/page-layout-settings.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-header-demo": { + name: "page-header-demo", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-header-demo")), + source: "", + files: ["registry/default/example/page-header-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-section-demo": { + name: "page-section-demo", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-section-demo")), + source: "", + files: ["registry/default/example/page-section-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-section-horizontal": { + name: "page-section-horizontal", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-section-horizontal")), + source: "", + files: ["registry/default/example/page-section-horizontal.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-section-with-aside": { + name: "page-section-with-aside", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-section-with-aside")), + source: "", + files: ["registry/default/example/page-section-with-aside.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "inner-side-menu-demo": { name: "inner-side-menu-demo", type: "components:example", @@ -2315,13 +2447,35 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, - "empty-state-initial-state": { - name: "empty-state-initial-state", + "empty-state-initial-state-presentational": { + name: "empty-state-initial-state-presentational", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/empty-state-initial-state-presentational")), + source: "", + files: ["registry/default/example/empty-state-initial-state-presentational.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "empty-state-initial-state-informational": { + name: "empty-state-initial-state-informational", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/empty-state-initial-state-informational")), + source: "", + files: ["registry/default/example/empty-state-initial-state-informational.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "empty-state-zero-items-data-grid": { + name: "empty-state-zero-items-data-grid", type: "components:example", registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/empty-state-initial-state")), + component: React.lazy(() => import("@/registry/default/example/empty-state-zero-items-data-grid")), source: "", - files: ["registry/default/example/empty-state-initial-state.tsx"], + files: ["registry/default/example/empty-state-zero-items-data-grid.tsx"], category: "undefined", subcategory: "undefined", chunks: [] diff --git a/apps/design-system/app/layout.tsx b/apps/design-system/app/layout.tsx index 9c005db6ae40f..72c8d4f5c0418 100644 --- a/apps/design-system/app/layout.tsx +++ b/apps/design-system/app/layout.tsx @@ -1,4 +1,5 @@ import '@/styles/globals.css' +import '../../studio/styles/typography.scss' import type { Metadata } from 'next' import { ThemeProvider } from './Providers' import { SonnerToaster } from './SonnerToast' diff --git a/apps/design-system/components/component-preview.tsx b/apps/design-system/components/component-preview.tsx index c1304ec5986d6..5a855c5f6843b 100644 --- a/apps/design-system/components/component-preview.tsx +++ b/apps/design-system/components/component-preview.tsx @@ -120,11 +120,11 @@ export function ComponentPreview({ ) }, [Preview, align]) - const wideClasses = wide ? '2xl:-ml-12 2xl:-mr-12' : '' + const wideClasses = wide ? '2xl:-ml-20 2xl:-mr-20' : '' if (peekCode) { return ( -
+
`](/design-system/docs/components/button) styles updated - -[PR](https://github.com/supabase/supabase/pull/27055) - -## 5th June 2024 - More Modal and Dialog examples - -[PR](https://github.com/supabase/supabase/pull/26844) - -## 3rd June 2024 - Add scroll example to NavigationMenu - -- [Added scroll example](https://supabase-design-system.vercel.app/design-system/docs/components/navigation-menu#with-horizontal-scroll) to [``](/design-system/docs/components/navigation-menu) - -## 25th May 2024 - TreeView component added - -- [``](/design-system/docs/components/tree-view) component added. - -[PR](https://github.com/supabase/supabase/pull/26821) - -## 24th May 2024 - FormItemLayout updated, and InfoTooltip added - -- [``](/design-system/docs/fragments/form-item-layout) now supports new props, `BeforeLabel` and `AfterLabel` -- [``](/design-system/docs/fragments/info-tooltip) added to easily add information tooltips - -[PR](https://github.com/supabase/supabase/pull/26712) - -## 24th May 2024 - Added Admonition component - -- [Admonition](/design-system/docs/fragments/admonition) - -[PR](https://github.com/supabase/supabase/pull/26710) - -## 24th May 2024 - New components and docs - -### New components added - -- [Multi Select](/design-system/components/multi-select) - -[PR](https://github.com/supabase/supabase/pull/26719) - -### New components documented - -- [Radio Group Card](/design-system/docs/components/radio-group-card) -- [Radio Group Stacked](/design-system/docs/components/radio-group-stacked) -- [Info Tooltip](/design-system/docs/components/info-tooltip) -- [Admonition](/design-system/docs/components/admonition) - -### New colors - -`bg-surface-75` tailwind color fixed for Dark theme - -## 22nd May 2024 - Introducing Design System - -We're introducing [Supabase Design System](/design-system), a home for Design related resources for Supabase diff --git a/apps/design-system/content/docs/figma.mdx b/apps/design-system/content/docs/figma.mdx deleted file mode 100644 index 35b3f81de8386..0000000000000 --- a/apps/design-system/content/docs/figma.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Figma -description: Every component recreated in Figma. With customizable props, typography and icons. ---- - -The Figma UI Kit is open sourced by [Pietro Schirano](https://twitter.com/skirano). - - - - - -## Additional resources - -### Figma Diagram Kit - - - - - -## Grab a copy - -https://www.figma.com/community/file/1203061493325953101 diff --git a/apps/design-system/content/docs/fragments/page-container.mdx b/apps/design-system/content/docs/fragments/page-container.mdx new file mode 100644 index 0000000000000..f71dc79be823f --- /dev/null +++ b/apps/design-system/content/docs/fragments/page-container.mdx @@ -0,0 +1,31 @@ +--- +title: Page Container +description: A container component that provides consistent max-width and padding based on size variants. +fragment: true +--- + + + +## Usage + +```tsx +import { PageContainer } from 'ui-patterns/PageContainer' +``` + +```tsx +{/* Page content */} +``` + +## Size Variants + +The `PageContainer` supports four size variants: + +- `small` - max-width: 768px +- `default` - max-width: 1200px +- `large` - max-width: 1600px +- `full` - no max-width constraint diff --git a/apps/design-system/content/docs/fragments/page-header.mdx b/apps/design-system/content/docs/fragments/page-header.mdx new file mode 100644 index 0000000000000..741efbbc0e12d --- /dev/null +++ b/apps/design-system/content/docs/fragments/page-header.mdx @@ -0,0 +1,24 @@ +--- +title: Page Header +description: A compound component for building page headers with consistent structure. +fragment: true +--- + + + +## Sub-components + +- `PageHeader` - Root container with size variants (`default`, `small`, `large`, `full`) +- `PageHeaderBreadcrumb` - Breadcrumb navigation wrapper (should be first child) +- `PageHeaderMeta` - Meta wrapper for icon, summary, and aside (groups icon and summary together, places aside on the right) +- `PageHeaderIcon` - Icon container positioned left of title (should be inside PageHeaderMeta, has `shrink-0`) +- `PageHeaderSummary` - Container for title and description (should be inside PageHeaderMeta, has `flex-1`) +- `PageHeaderTitle` - Primary page heading (h1) +- `PageHeaderDescription` - Supporting text below title +- `PageHeaderAside` - Container for action buttons (should be inside PageHeaderMeta, has `shrink-0`) +- `PageHeaderFooter` - Container for tab navigation (NavMenu, should be last child) diff --git a/apps/design-system/content/docs/fragments/page-section.mdx b/apps/design-system/content/docs/fragments/page-section.mdx new file mode 100644 index 0000000000000..bc70b83723a32 --- /dev/null +++ b/apps/design-system/content/docs/fragments/page-section.mdx @@ -0,0 +1,47 @@ +--- +title: Page Section +description: A compound component for organizing page content into distinct sections. +fragment: true +--- + + + +## Sub-components + +- `PageSection` - Root container with orientation variants (`horizontal` or `vertical`) +- `PageSectionMeta` - Meta wrapper for summary and aside (groups summary and aside together) +- `PageSectionSummary` - Container for section title and description (should be inside PageSectionMeta, has `flex-1`) +- `PageSectionTitle` - Section heading (h2) +- `PageSectionDescription` - Supporting text below section title +- `PageSectionAside` - Container for section-level actions (should be inside PageSectionMeta, has `shrink-0`) +- `PageSectionContent` - Container for the main section content + +## Orientation Variants + +- `vertical` - Content stacks vertically (default) +- `horizontal` - Summary and content arranged horizontally on larger screens + +## Examples + +### Horizontal Orientation + + + +### With Aside Actions + + 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 876f5020a0eee..53a3a8d96be4e 100644 --- a/apps/design-system/content/docs/ui-patterns/empty-states.mdx +++ b/apps/design-system/content/docs/ui-patterns/empty-states.mdx @@ -3,35 +3,60 @@ title: Empty states description: Convey the absence of data and provide clear instruction for what to do about it. --- -At a minimum, empty states convey the fact that there is nothing to list, perform, or display on the current page. They should also provide a clear call to action for the user to take. +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.. +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. -## Zero results +## No data -Tabular information without results—or perhaps no data to begin with—should have an empty state that matches the larger presentation. +There are two ways an empty state may be displayed in cases where there is no data: -For instance, a [Table](../components/table) may just display a single row just like it would if it had data. Dulling the TableHead text color and removing the TableCell hover state can further reinforce the lack of usable 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. -The treatment for other layouts, such as the list of users in [Authentication](https://supabase.com/dashboard/project/_/auth/users), should match their own general styling. +#### 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). + +### Initial state + +Perhaps the user has not yet created any data. The presentation of this empty state depends on the context of the list and the type of data it contains. + +#### Presentational + +The user may be learning about a feature for the first time, and this experience benefits from user education or onboarding. In cases like these, the empty state should put more focus on the feature’s value proposition and an action the user can take. + + -## Initial state +#### Informational -Perhaps the user has not yet created any data yet. They might be a feature for the first time. In these cases, the empty state should provide the briefest information about the lack of data, putting more focus on the value proposition and primary action. +Or perhaps the list type is data-heavy or does not benefit from additional information. In these cases, the empty state should provide show the initial state in the same presentation as the list when there is data, much like the [zero results](#zero-results) scenario. - + -Keep in mind that this empty state will likely appear after a visual loading state. Consider layout shift and button placement during and after the transition. +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. ## Components -There is not yet a shared empty state UI component. The context and needs for each placement differ enough to warrant custom components for each placement. That said, we should aim to make these as consistent as possible over time. See the below examples that might share common logic in a future centralized component. +There is not yet a shared empty state UI component. The context and needs for each placement differ enough to warrant custom components for each placement. ## External references diff --git a/apps/design-system/content/docs/ui-patterns/layout.mdx b/apps/design-system/content/docs/ui-patterns/layout.mdx new file mode 100644 index 0000000000000..e69716e597eaf --- /dev/null +++ b/apps/design-system/content/docs/ui-patterns/layout.mdx @@ -0,0 +1,80 @@ +--- +title: Layout +description: Guidelines to create consistent layouts across Studio pages using a set of page components. +--- + +The Page pattern consists of three main components that work together to create consistent page layouts: [PageContainer](../fragments/page-container), [PageHeader](../fragments/page-header), and [PageSection](../fragments/page-section). These components provide a structured approach to building pages with consistent spacing, max-widths, and content organization. + +## Layout Types + +### Settings + +Settings pages are used for configuration and preference management. They follow a single-column layout with default widths to keep content focused and readable. Examples include project settings or auth sessions. + +- Use `PageHeader` with `size="default"` +- Use `PageContainer` with `size="default"` +- Use `PageSection` for organizing settings into logical groups + + + +### List + +List pages display collections of objects like tables, triggers, or functions. These pages use larger widths to accommodate wide content like data tables. Examples include database triggers, database functions or org team members. + +- Use `PageHeader` with `size="large"` +- Use `PageContainer` with `size="large"` +- Use `PageSection` to wrap list content + +**Table and List Actions:** + +- **With filters or search:** If the table has filters or search, place table actions aligned with the filters on the right side. Do not use `PageSectionAside` or `PageHeaderAside` for table actions when filters are present. +- **Without filters:** For simple lists without filters or search, add primary list actions to `PageHeaderAside` or `PageSectionAside` as appropriate. + + + + + +### Data and Full Page Experiences + +Full-page experiences like the table editor, cron jobs, and edge functions require maximum screen real-estate and so make use of "full" size containers. + +- Use `PageHeader` with `size="full"` +- Use `PageContainer` with `size="full"` +- Content spans the full width of the viewport + +### Detail Pages + +Detail pages display dense or lengthy content split into multiple sections. The horizontal orientation allows for better information hierarchy and context. Examples include organisation billing or project infrastructure. + +- Use `PageHeader` with `size="large"` +- Use `PageContainer` with `size="large"` +- Use `PageSection` with `orientation="horizontal"` to show summary alongside content +- Multiple sections can stack vertically with horizontal layouts within each + + + +## Components + +- **[PageContainer](/docs/fragments/page-container)** - Container component providing consistent max-width and padding based on size variants +- **[PageHeader](/docs/fragments/page-header)** - Compound component for building page headers with breadcrumbs, icons, titles, descriptions, actions, and navigation +- **[PageSection](/docs/fragments/page-section)** - Compound component for organizing page content into distinct sections with title, description, and action areas diff --git a/apps/design-system/package.json b/apps/design-system/package.json index 928d7cc74dc57..32c711e38c235 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -29,6 +29,7 @@ "next-contentlayer2": "0.4.6", "next-themes": "^0.3.0", "react": "catalog:", + "react-data-grid": "7.0.0-beta.41", "react-day-picker": "^9.11.1", "react-docgen": "^7.0.3", "react-dom": "catalog:", @@ -51,6 +52,7 @@ }, "devDependencies": { "@shikijs/compat": "^1.1.7", + "@tailwindcss/container-queries": "^0.1.1", "@types/lodash.template": "4.5.0", "@types/react": "catalog:", "@types/react-dom": "catalog:", diff --git a/apps/design-system/registry/default/example/empty-state-initial-state-informational.tsx b/apps/design-system/registry/default/example/empty-state-initial-state-informational.tsx new file mode 100644 index 0000000000000..5c45d693e64e0 --- /dev/null +++ b/apps/design-system/registry/default/example/empty-state-initial-state-informational.tsx @@ -0,0 +1,31 @@ +import { Plus } from 'lucide-react' +import { Button, Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui' + +export default function EmptyStateZeroItemsTable() { + return ( +
+ + + + + + Table name + Date created + + + + + + +

No tables yet

+

Create a table to get started

+
+
+
+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/empty-state-initial-state.tsx b/apps/design-system/registry/default/example/empty-state-initial-state-presentational.tsx similarity index 92% rename from apps/design-system/registry/default/example/empty-state-initial-state.tsx rename to apps/design-system/registry/default/example/empty-state-initial-state-presentational.tsx index 0c0db65716615..81ace8d4713a5 100644 --- a/apps/design-system/registry/default/example/empty-state-initial-state.tsx +++ b/apps/design-system/registry/default/example/empty-state-initial-state-presentational.tsx @@ -1,8 +1,8 @@ -import { Button } from 'ui' -import { Plus } from 'lucide-react' import { BucketAdd } from 'icons' +import { Plus } from 'lucide-react' +import { Button } from 'ui' -export default function EmptyStateInitialState() { +export default function EmptyStateInitialStatePresentational() { return (
+ + + + {navigationItems.length > 0 && ( + + + {navigationItems.map((item) => { + const isActive = router.asPath.split('?')[0] === item.href + return ( + + {item.label} + + ) + })} + + + )} + + {children} setIsOpen(false)} /> - +
) } diff --git a/apps/studio/components/ui/AdvisorPanel/AdvisorDetail.tsx b/apps/studio/components/ui/AdvisorPanel/AdvisorDetail.tsx index cc124a5c69c14..b6c2356631d72 100644 --- a/apps/studio/components/ui/AdvisorPanel/AdvisorDetail.tsx +++ b/apps/studio/components/ui/AdvisorPanel/AdvisorDetail.tsx @@ -2,7 +2,7 @@ import LintDetail from 'components/interfaces/Linter/LintDetail' import { Lint } from 'data/lint/lint-query' import { Notification } from 'data/notifications/notifications-v2-query' import { noop } from 'lodash' -import { AdvisorItem } from './AdvisorPanelHeader' +import type { AdvisorItem } from './AdvisorPanel.types' import { NotificationDetail } from './NotificationDetail' interface AdvisorDetailProps { diff --git a/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.tsx b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.tsx index 23d798e7deaa1..cd57b2400be52 100644 --- a/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.tsx +++ b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.tsx @@ -16,8 +16,9 @@ import { AdvisorSeverity, AdvisorTab, useAdvisorStateSnapshot } from 'state/advi import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' import { AdvisorDetail } from './AdvisorDetail' import { AdvisorFilters } from './AdvisorFilters' +import type { AdvisorItem } from './AdvisorPanel.types' import { AdvisorPanelBody } from './AdvisorPanelBody' -import { AdvisorItem, AdvisorPanelHeader } from './AdvisorPanelHeader' +import { AdvisorPanelHeader } from './AdvisorPanelHeader' const severityOrder: Record = { critical: 0, diff --git a/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.types.ts b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.types.ts new file mode 100644 index 0000000000000..3c24a57fe2957 --- /dev/null +++ b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.types.ts @@ -0,0 +1,13 @@ +import { Lint } from 'data/lint/lint-query' +import { Notification } from 'data/notifications/notifications-v2-query' +import { AdvisorItemSource, AdvisorSeverity } from 'state/advisor-state' + +export type AdvisorItem = { + id: string + title: string + severity: AdvisorSeverity + createdAt?: number + tab: 'security' | 'performance' | 'messages' + source: AdvisorItemSource + original: Lint | Notification +} diff --git a/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.utils.ts b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.utils.ts new file mode 100644 index 0000000000000..19a9bbe53f570 --- /dev/null +++ b/apps/studio/components/ui/AdvisorPanel/AdvisorPanel.utils.ts @@ -0,0 +1,65 @@ +import dayjs from 'dayjs' +import { Gauge, Inbox, Shield } from 'lucide-react' + +import { lintInfoMap } from 'components/interfaces/Linter/Linter.utils' +import { Lint } from 'data/lint/lint-query' +import { AdvisorSeverity, AdvisorTab } from 'state/advisor-state' +import type { AdvisorItem } from './AdvisorPanel.types' + +export const formatItemDate = (timestamp: number): string => { + const daysFromNow = dayjs().diff(dayjs(timestamp), 'day') + const formattedTimeFromNow = dayjs(timestamp).fromNow() + const formattedInsertedAt = dayjs(timestamp).format('MMM DD, YYYY') + return daysFromNow > 1 ? formattedInsertedAt : formattedTimeFromNow +} + +export const getAdvisorItemDisplayTitle = (item: AdvisorItem): string => { + if (item.source === 'lint') { + const lint = item.original as Lint + return ( + lintInfoMap.find((info) => info.name === lint.name)?.title || item.title.replace(/[`\\]/g, '') + ) + } + return item.title.replace(/[`\\]/g, '') +} + +export const tabIconMap: Record, React.ElementType> = { + security: Shield, + performance: Gauge, + messages: Inbox, +} + +export const severityColorClasses: Record = { + critical: 'text-destructive', + warning: 'text-warning', + info: 'text-foreground-light', +} + +export const severityBadgeVariants: Record = + { + critical: 'destructive', + warning: 'warning', + info: 'default', + } + +export const severityLabels: Record = { + critical: 'Critical', + warning: 'Warning', + info: 'Info', +} + +export const getLintEntityString = (lint: Lint | null): string | undefined => { + if (!lint?.metadata) { + return undefined + } + + if (lint.metadata.entity) { + return lint.metadata.entity + } + + if (lint.metadata.schema && lint.metadata.name) { + return `${lint.metadata.schema}.${lint.metadata.name}` + } + + return undefined +} diff --git a/apps/studio/components/ui/AdvisorPanel/AdvisorPanelBody.tsx b/apps/studio/components/ui/AdvisorPanel/AdvisorPanelBody.tsx index 7b00fce3e6aa3..a12326e244782 100644 --- a/apps/studio/components/ui/AdvisorPanel/AdvisorPanelBody.tsx +++ b/apps/studio/components/ui/AdvisorPanel/AdvisorPanelBody.tsx @@ -1,11 +1,20 @@ -import dayjs from 'dayjs' -import { AlertTriangle, ChevronRight, Gauge, Inbox, Shield } from 'lucide-react' +import { AlertTriangle, ChevronRight, Inbox } from 'lucide-react' +import { Lint } from 'data/lint/lint-query' import { Notification } from 'data/notifications/notifications-v2-query' import { AdvisorSeverity, AdvisorTab } from 'state/advisor-state' -import { Button, cn } from 'ui' +import { Badge, Button, cn } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns' -import { AdvisorItem } from './AdvisorPanelHeader' +import type { AdvisorItem } from './AdvisorPanel.types' +import { + formatItemDate, + getAdvisorItemDisplayTitle, + getLintEntityString, + severityBadgeVariants, + severityColorClasses, + severityLabels, + tabIconMap, +} from './AdvisorPanel.utils' import { EmptyAdvisor } from './EmptyAdvisor' const NoProjectNotice = () => { @@ -22,18 +31,6 @@ const NoProjectNotice = () => { ) } -const tabIconMap: Record, React.ElementType> = { - security: Shield, - performance: Gauge, - messages: Inbox, -} - -const severityColorClasses: Record = { - critical: 'text-destructive', - warning: 'text-warning', - info: 'text-foreground-light', -} - interface AdvisorPanelBodyProps { isLoading: boolean isError: boolean @@ -96,11 +93,19 @@ export const AdvisorPanelBody = ({ <>
{filteredItems.map((item) => { - const SeverityIcon = tabIconMap[item.tab] + const SeverityIcon = tabIconMap[item.tab as Exclude] const severityClass = severityColorClasses[item.severity] const isNotification = item.source === 'notification' const notification = isNotification ? (item.original as Notification) : null const isUnread = notification?.status === 'new' + const lint = !isNotification ? (item.original as Lint) : null + + // Primary text: issue type for lint items, title for notifications + const primaryText = getAdvisorItemDisplayTitle(item) + + // Secondary text: entity for lint items when no date, date for notifications + const hasDate = !!item.createdAt + const entityString = getLintEntityString(lint) return (
@@ -113,32 +118,39 @@ export const AdvisorPanelBody = ({ onClick={() => onItemClick(item)} >
-
+
-
{item.title.replace(/[`\\]/g, '')}
- {item.createdAt && ( +
{primaryText}
+ {hasDate ? ( - {(() => { - const insertedAt = item.createdAt - const daysFromNow = dayjs().diff(dayjs(insertedAt), 'day') - const formattedTimeFromNow = dayjs(insertedAt).fromNow() - const formattedInsertedAt = dayjs(insertedAt).format('MMM DD, YYYY') - return daysFromNow > 1 ? formattedInsertedAt : formattedTimeFromNow - })()} + {formatItemDate(item.createdAt!)} + ) : ( + entityString && ( +
+ {entityString} +
+ ) )}
- +
+ {item.severity === 'critical' && ( + + {severityLabels[item.severity]} + + )} + +
diff --git a/apps/studio/components/ui/AdvisorPanel/AdvisorPanelHeader.tsx b/apps/studio/components/ui/AdvisorPanel/AdvisorPanelHeader.tsx index ae060dddb9355..cb15bcef32b2e 100644 --- a/apps/studio/components/ui/AdvisorPanel/AdvisorPanelHeader.tsx +++ b/apps/studio/components/ui/AdvisorPanel/AdvisorPanelHeader.tsx @@ -1,32 +1,14 @@ -import dayjs from 'dayjs' import { ChevronLeft, X } from 'lucide-react' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { AdvisorItemSource, AdvisorSeverity } from 'state/advisor-state' import { Badge } from 'ui' - -export type AdvisorItem = { - id: string - title: string - severity: AdvisorSeverity - createdAt?: number - tab: 'security' | 'performance' | 'messages' - source: AdvisorItemSource - original: any -} - -export const severityBadgeVariants: Record = - { - critical: 'destructive', - warning: 'warning', - info: 'default', - } - -export const severityLabels: Record = { - critical: 'Critical', - warning: 'Warning', - info: 'Info', -} +import type { AdvisorItem } from './AdvisorPanel.types' +import { + formatItemDate, + getAdvisorItemDisplayTitle, + severityBadgeVariants, + severityLabels, +} from './AdvisorPanel.utils' interface AdvisorPanelHeaderProps { selectedItem: AdvisorItem | undefined @@ -35,6 +17,8 @@ interface AdvisorPanelHeaderProps { } export const AdvisorPanelHeader = ({ selectedItem, onBack, onClose }: AdvisorPanelHeaderProps) => { + const displayTitle = selectedItem ? getAdvisorItemDisplayTitle(selectedItem) : undefined + return (
- {selectedItem?.title?.replace(/[`\\]/g, '')} + {displayTitle} {selectedItem?.createdAt && ( - {(() => { - const insertedAt = selectedItem.createdAt - const daysFromNow = dayjs().diff(dayjs(insertedAt), 'day') - const formattedTimeFromNow = dayjs(insertedAt).fromNow() - const formattedInsertedAt = dayjs(insertedAt).format('MMM DD, YYYY') - return daysFromNow > 1 ? formattedInsertedAt : formattedTimeFromNow - })()} + {formatItemDate(selectedItem.createdAt)} )}
diff --git a/apps/studio/data/etl/pipeline-status-query.ts b/apps/studio/data/etl/pipeline-status-query.ts index 1e6b2d9787d17..6367239c4f4f0 100644 --- a/apps/studio/data/etl/pipeline-status-query.ts +++ b/apps/studio/data/etl/pipeline-status-query.ts @@ -1,10 +1,13 @@ import { useQuery } from '@tanstack/react-query' +import { components } from 'api-types' import { get, handleError } from 'data/fetchers' import type { ResponseError, UseCustomQueryOptions } from 'types' import { replicationKeys } from './keys' type ReplicationPipelinesStatusParams = { projectRef?: string; pipelineId?: number } +type ReplicationPipelineStatusResponse = components['schemas']['ReplicationPipelineStatusResponse'] +export type ReplicationPipelineStatus = ReplicationPipelineStatusResponse['status']['name'] async function fetchReplicationPipelineStatus( { projectRef, pipelineId }: ReplicationPipelinesStatusParams, diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx index 8033823a5047e..44850089399b3 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx @@ -3,11 +3,7 @@ import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionDetailsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout' import type { NextPageWithLayout } from 'types' -const PageLayout: NextPageWithLayout = () => ( -
- -
-) +const PageLayout: NextPageWithLayout = () => PageLayout.getLayout = (page) => ( diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx index e09f4959457a0..dd26dee0ae5c1 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx @@ -29,6 +29,8 @@ import { Button, WarningIcon, } from 'ui' +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' const CHART_INTERVALS: ChartIntervals[] = [ { @@ -131,141 +133,260 @@ const PageLayout: NextPageWithLayout = () => { } return ( -
-
-
-
- {CHART_INTERVALS.map((item, i) => { - const classes = [] + + + +
+
+ {CHART_INTERVALS.map((item, i) => { + const classes = [] - if (i === 0) { - classes.push('rounded-tr-none rounded-br-none') - } else if (i === CHART_INTERVALS.length - 1) { - classes.push('rounded-tl-none rounded-bl-none') - } else { - classes.push('rounded-none') - } + if (i === 0) { + classes.push('rounded-tr-none rounded-br-none') + } else if (i === CHART_INTERVALS.length - 1) { + classes.push('rounded-tl-none rounded-bl-none') + } else { + classes.push('rounded-none') + } - return ( - - ) - })} -
+ return ( + + ) + })} +
- - Statistics for past {selectedInterval.label} - -
-
-
- { - return isErrorCombinedStats ? ( - - - Failed to reterieve execution time - - {combinedStatsError.message} - - - ) : ( -
- - {newChartsEnabled && ( + + Statistics for past {selectedInterval.label} + +
+
+
+ { + return isErrorCombinedStats ? ( + + + Failed to reterieve execution time + + {combinedStatsError.message} + + + ) : ( +
- )} -
- ) - }} - /> - { - if (isErrorCombinedStats) { - return ( + {newChartsEnabled && ( + + )} +
+ ) + }} + /> + { + if (isErrorCombinedStats) { + return ( + + + Failed to reterieve invocations + + {combinedStatsError.message} + + + ) + } else { + const requestData = props.data + .map((d: any) => [ + { + status: '2xx', + count: d.success_count, + timestamp: d.timestamp, + }, + { + status: '3xx', + count: d.redirect_count, + timestamp: d.timestamp, + }, + { + status: '4xx', + count: d.client_err_count, + timestamp: d.timestamp, + }, + { + status: '5xx', + count: d.server_err_count, + timestamp: d.timestamp, + }, + ]) + .flat() + + const logsData = props.data + .map((d: any) => [ + { + status: 'error', + count: d.log_error_count, + timestamp: d.timestamp, + }, + { + status: 'info', + count: d.log_info_count, + timestamp: d.timestamp, + }, + { + status: 'warn', + count: d.log_warn_count, + timestamp: d.timestamp, + }, + ]) + .flat() + + return ( +
+ { + router.push( + `/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}` + ) + }} + /> + {newChartsEnabled && ( + { + router.push( + `/project/${projectRef}/functions/${functionSlug}/logs?its=${startDate.toISOString()}` + ) + }} + /> + )} +
+ ) + } + }} + /> + { + return isErrorCombinedStats ? ( - Failed to reterieve invocations + Failed to retrieve CPU time {combinedStatsError.message} + ) : ( +
+ + {newChartsEnabled && ( + + )} +
) - } else { - const requestData = props.data - .map((d: any) => [ - { - status: '2xx', - count: d.success_count, - timestamp: d.timestamp, - }, - { - status: '3xx', - count: d.redirect_count, - timestamp: d.timestamp, - }, - { - status: '4xx', - count: d.client_err_count, - timestamp: d.timestamp, - }, - { - status: '5xx', - count: d.server_err_count, - timestamp: d.timestamp, - }, - ]) - .flat() + }} + /> + { + if (isErrorCombinedStats) { + return ( + + + Failed to retrieve memory usage + + {combinedStatsError.message} + + + ) + } - const logsData = props.data + const memoryData = props.data .map((d: any) => [ { - status: 'error', - count: d.log_error_count, - timestamp: d.timestamp, - }, - { - status: 'info', - count: d.log_info_count, + type: 'heap', + count: d.avg_heap_memory_used, timestamp: d.timestamp, }, { - status: 'warn', - count: d.log_warn_count, + type: 'external', + count: d.avg_external_memory_used, timestamp: d.timestamp, }, ]) @@ -273,154 +394,39 @@ const PageLayout: NextPageWithLayout = () => { return (
- { - router.push( - `/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}` - ) - }} + yAxisKey="avg_memory_used" + data={props.data} + format="MB" + highlightedValue={meanBy(props.data, 'avg_memory_used')} /> {newChartsEnabled && ( { - router.push( - `/project/${projectRef}/functions/${functionSlug}/logs?its=${startDate.toISOString()}` - ) - }} + stackColors={['blue', 'brand']} /> )}
) - } - }} - /> - { - return isErrorCombinedStats ? ( - - - Failed to retrieve CPU time - - {combinedStatsError.message} - - - ) : ( -
- - {newChartsEnabled && ( - - )} -
- ) - }} - /> - { - if (isErrorCombinedStats) { - return ( - - - Failed to retrieve memory usage - - {combinedStatsError.message} - - - ) - } - - const memoryData = props.data - .map((d: any) => [ - { - type: 'heap', - count: d.avg_heap_memory_used, - timestamp: d.timestamp, - }, - { - type: 'external', - count: d.avg_external_memory_used, - timestamp: d.timestamp, - }, - ]) - .flat() - - return ( -
- - {newChartsEnabled && ( - - )} -
- ) - }} - /> + }} + /> +
-
-
-
+ + + ) } diff --git a/apps/studio/pages/project/[ref]/functions/index.tsx b/apps/studio/pages/project/[ref]/functions/index.tsx index 9d68e2471ed22..0988ae64ace94 100644 --- a/apps/studio/pages/project/[ref]/functions/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/index.tsx @@ -1,4 +1,5 @@ import { ExternalLink } from 'lucide-react' +import React from 'react' import { useParams } from 'common' import { DeployEdgeFunctionButton } from 'components/interfaces/EdgeFunctions/DeployEdgeFunctionButton' @@ -9,8 +10,6 @@ import { } from 'components/interfaces/Functions/FunctionsEmptyState' import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout' -import { PageLayout } from 'components/layouts/PageLayout/PageLayout' -import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' @@ -18,6 +17,16 @@ import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' import { DOCS_URL, IS_PLATFORM } from 'lib/constants' import type { NextPageWithLayout } from 'types' import { Button, Card, Table, TableBody, TableHead, TableHeader, TableRow } from 'ui' +import { PageContainer } from 'ui-patterns/PageContainer' +import { + PageHeader, + PageHeaderAside, + PageHeaderDescription, + PageHeaderMeta, + PageHeaderSummary, + PageHeaderTitle, +} from 'ui-patterns/PageHeader' +import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' const EdgeFunctionsPage: NextPageWithLayout = () => { const { ref } = useParams() @@ -31,29 +40,10 @@ const EdgeFunctionsPage: NextPageWithLayout = () => { const hasFunctions = (functions ?? []).length > 0 - const secondaryActions = [ - , - , - ] - return ( - : undefined} - secondaryActions={secondaryActions} - > - - + + + {IS_PLATFORM ? ( <> {isLoading && } @@ -92,16 +82,54 @@ const EdgeFunctionsPage: NextPageWithLayout = () => { ) : ( )} - - - + + + ) } -EdgeFunctionsPage.getLayout = (page) => { +EdgeFunctionsPage.getLayout = (page: React.ReactElement) => { + const EdgeFunctionsPageLayout = () => { + const secondaryActions = [ + , + , + ] + + return ( +
+ + + + Edge Functions + + Deploy edge functions to handle complex business logic + + + + {secondaryActions.map((action) => action)} + {IS_PLATFORM && } + + + + + {page} +
+ ) + } + return ( - {page} + + + ) } diff --git a/apps/studio/state/replication-pipeline-request-status.tsx b/apps/studio/state/replication-pipeline-request-status.tsx index 36561ab544440..b14c7864ebd1a 100644 --- a/apps/studio/state/replication-pipeline-request-status.tsx +++ b/apps/studio/state/replication-pipeline-request-status.tsx @@ -1,11 +1,11 @@ import { createContext, - useContext, - useState, ReactNode, useCallback, - useRef, + useContext, useEffect, + useRef, + useState, } from 'react' export enum PipelineStatusRequestStatus { @@ -35,6 +35,9 @@ const PipelineRequestStatusContext = createContext { const [requestStatus, setRequestStatusState] = useState< Record diff --git a/packages/ui-patterns/index.tsx b/packages/ui-patterns/index.tsx index f33a01ebe8d70..6d6b600d4a364 100644 --- a/packages/ui-patterns/index.tsx +++ b/packages/ui-patterns/index.tsx @@ -12,6 +12,9 @@ export * from './src/FilterBar' export * from './src/GlassPanel' export * from './src/InnerSideMenu' export * from './src/McpUrlBuilder' +export * from './src/PageContainer' +export * from './src/PageHeader' +export * from './src/PageSection' export * from './src/PopupFrame' export * from './src/ShimmeringLoader' export * from './src/TimestampInfo' diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index 7d4cc23fdf282..39e7e8f6034d9 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -34,17 +34,21 @@ "import": "./src/AuthenticatedDropdownMenu/index.tsx", "types": "./src/AuthenticatedDropdownMenu/index.tsx" }, + "./Banners/AnnouncementBanner": { + "import": "./src/Banners/AnnouncementBanner.tsx", + "types": "./src/Banners/AnnouncementBanner.tsx" + }, "./Banners/Countdown": { "import": "./src/Banners/Countdown.tsx", "types": "./src/Banners/Countdown.tsx" }, - "./Banners/LW14Announcement": { - "import": "./src/Banners/LW14Announcement.tsx", - "types": "./src/Banners/LW14Announcement.tsx" + "./Banners/LW15Banner": { + "import": "./src/Banners/LW15Banner.tsx", + "types": "./src/Banners/LW15Banner.tsx" }, - "./Banners/LW14Banner": { - "import": "./src/Banners/LW14Banner.tsx", - "types": "./src/Banners/LW14Banner.tsx" + "./Banners": { + "import": "./src/Banners/index.ts", + "types": "./src/Banners/index.ts" }, "./CommandMenu/api/Badges": { "import": "./src/CommandMenu/api/Badges.tsx", @@ -98,6 +102,14 @@ "import": "./src/CommandMenu/api/hooks/useCommandFilterState.ts", "types": "./src/CommandMenu/api/hooks/useCommandFilterState.ts" }, + "./CommandMenu/api/hooks/useCommandMenuTelemetry": { + "import": "./src/CommandMenu/api/hooks/useCommandMenuTelemetry.ts", + "types": "./src/CommandMenu/api/hooks/useCommandMenuTelemetry.ts" + }, + "./CommandMenu/api/hooks/useCommandMenuTelemetryContext": { + "import": "./src/CommandMenu/api/hooks/useCommandMenuTelemetryContext.tsx", + "types": "./src/CommandMenu/api/hooks/useCommandMenuTelemetryContext.tsx" + }, "./CommandMenu/api/hooks/useCrossCompatRouter": { "import": "./src/CommandMenu/api/hooks/useCrossCompatRouter.tsx", "types": "./src/CommandMenu/api/hooks/useCrossCompatRouter.tsx" @@ -214,6 +226,10 @@ "import": "./src/CommandMenu/prepackaged/ai/queryAi.ts", "types": "./src/CommandMenu/prepackaged/ai/queryAi.ts" }, + "./CommandMenu/prepackaged/ai/useAiChat.test": { + "import": "./src/CommandMenu/prepackaged/ai/useAiChat.test.ts", + "types": "./src/CommandMenu/prepackaged/ai/useAiChat.test.ts" + }, "./CommandMenu/prepackaged/ai/useAiChat": { "import": "./src/CommandMenu/prepackaged/ai/useAiChat.ts", "types": "./src/CommandMenu/prepackaged/ai/useAiChat.ts" @@ -278,6 +294,14 @@ "import": "./src/ExpandableVideo/index.tsx", "types": "./src/ExpandableVideo/index.tsx" }, + "./FilterBar/DefaultCommandList": { + "import": "./src/FilterBar/DefaultCommandList.tsx", + "types": "./src/FilterBar/DefaultCommandList.tsx" + }, + "./FilterBar/FilterBar.test": { + "import": "./src/FilterBar/FilterBar.test.tsx", + "types": "./src/FilterBar/FilterBar.test.tsx" + }, "./FilterBar/FilterBar": { "import": "./src/FilterBar/FilterBar.tsx", "types": "./src/FilterBar/FilterBar.tsx" @@ -290,14 +314,50 @@ "import": "./src/FilterBar/FilterGroup.tsx", "types": "./src/FilterBar/FilterGroup.tsx" }, + "./FilterBar/hooks.test": { + "import": "./src/FilterBar/hooks.test.ts", + "types": "./src/FilterBar/hooks.test.ts" + }, + "./FilterBar/hooks": { + "import": "./src/FilterBar/hooks.ts", + "types": "./src/FilterBar/hooks.ts" + }, "./FilterBar": { "import": "./src/FilterBar/index.ts", "types": "./src/FilterBar/index.ts" }, + "./FilterBar/menuItems": { + "import": "./src/FilterBar/menuItems.ts", + "types": "./src/FilterBar/menuItems.ts" + }, "./FilterBar/types": { "import": "./src/FilterBar/types.ts", "types": "./src/FilterBar/types.ts" }, + "./FilterBar/useAIFilter": { + "import": "./src/FilterBar/useAIFilter.ts", + "types": "./src/FilterBar/useAIFilter.ts" + }, + "./FilterBar/useCommandHandling": { + "import": "./src/FilterBar/useCommandHandling.ts", + "types": "./src/FilterBar/useCommandHandling.ts" + }, + "./FilterBar/useCommandMenu": { + "import": "./src/FilterBar/useCommandMenu.ts", + "types": "./src/FilterBar/useCommandMenu.ts" + }, + "./FilterBar/useKeyboardNavigation": { + "import": "./src/FilterBar/useKeyboardNavigation.ts", + "types": "./src/FilterBar/useKeyboardNavigation.ts" + }, + "./FilterBar/utils.test": { + "import": "./src/FilterBar/utils.test.ts", + "types": "./src/FilterBar/utils.test.ts" + }, + "./FilterBar/utils": { + "import": "./src/FilterBar/utils.ts", + "types": "./src/FilterBar/utils.ts" + }, "./GlassPanel": { "import": "./src/GlassPanel/index.tsx", "types": "./src/GlassPanel/index.tsx" @@ -314,10 +374,46 @@ "import": "./src/LogsBarChart/index.tsx", "types": "./src/LogsBarChart/index.tsx" }, + "./McpUrlBuilder/McpConfigPanel": { + "import": "./src/McpUrlBuilder/McpConfigPanel.tsx", + "types": "./src/McpUrlBuilder/McpConfigPanel.tsx" + }, + "./McpUrlBuilder/components/ClientSelectDropdown": { + "import": "./src/McpUrlBuilder/components/ClientSelectDropdown.tsx", + "types": "./src/McpUrlBuilder/components/ClientSelectDropdown.tsx" + }, + "./McpUrlBuilder/components/ConnectionIcon": { + "import": "./src/McpUrlBuilder/components/ConnectionIcon.tsx", + "types": "./src/McpUrlBuilder/components/ConnectionIcon.tsx" + }, + "./McpUrlBuilder/components/McpConfigurationDisplay": { + "import": "./src/McpUrlBuilder/components/McpConfigurationDisplay.tsx", + "types": "./src/McpUrlBuilder/components/McpConfigurationDisplay.tsx" + }, + "./McpUrlBuilder/components/McpConfigurationOptions": { + "import": "./src/McpUrlBuilder/components/McpConfigurationOptions.tsx", + "types": "./src/McpUrlBuilder/components/McpConfigurationOptions.tsx" + }, + "./McpUrlBuilder/constants": { + "import": "./src/McpUrlBuilder/constants.tsx", + "types": "./src/McpUrlBuilder/constants.tsx" + }, "./McpUrlBuilder": { "import": "./src/McpUrlBuilder/index.ts", "types": "./src/McpUrlBuilder/index.ts" }, + "./McpUrlBuilder/types": { + "import": "./src/McpUrlBuilder/types.ts", + "types": "./src/McpUrlBuilder/types.ts" + }, + "./McpUrlBuilder/utils/getMcpButtonData": { + "import": "./src/McpUrlBuilder/utils/getMcpButtonData.ts", + "types": "./src/McpUrlBuilder/utils/getMcpButtonData.ts" + }, + "./McpUrlBuilder/utils/getMcpUrl": { + "import": "./src/McpUrlBuilder/utils/getMcpUrl.ts", + "types": "./src/McpUrlBuilder/utils/getMcpUrl.ts" + }, "./MobileSheetNav/MobileSheetNav": { "import": "./src/MobileSheetNav/MobileSheetNav.tsx", "types": "./src/MobileSheetNav/MobileSheetNav.tsx" @@ -338,6 +434,22 @@ "import": "./src/MultiSelectDeprecated/index.tsx", "types": "./src/MultiSelectDeprecated/index.tsx" }, + "./PageContainer": { + "import": "./src/PageContainer/index.tsx", + "types": "./src/PageContainer/index.tsx" + }, + "./PageHeader": { + "import": "./src/PageHeader/index.tsx", + "types": "./src/PageHeader/index.tsx" + }, + "./PageSection": { + "import": "./src/PageSection/index.tsx", + "types": "./src/PageSection/index.tsx" + }, + "./PopupFrame": { + "import": "./src/PopupFrame/index.tsx", + "types": "./src/PopupFrame/index.tsx" + }, "./PrivacySettings": { "import": "./src/PrivacySettings/index.tsx", "types": "./src/PrivacySettings/index.tsx" @@ -354,6 +466,14 @@ "import": "./src/PromoToast/index.ts", "types": "./src/PromoToast/index.ts" }, + "./Row/Row.utils": { + "import": "./src/Row/Row.utils.ts", + "types": "./src/Row/Row.utils.ts" + }, + "./Row": { + "import": "./src/Row/index.tsx", + "types": "./src/Row/index.tsx" + }, "./ShimmeringLoader": { "import": "./src/ShimmeringLoader/index.tsx", "types": "./src/ShimmeringLoader/index.tsx" diff --git a/packages/ui-patterns/src/PageContainer/index.tsx b/packages/ui-patterns/src/PageContainer/index.tsx new file mode 100644 index 0000000000000..526e650608689 --- /dev/null +++ b/packages/ui-patterns/src/PageContainer/index.tsx @@ -0,0 +1,51 @@ +import { cva, type VariantProps } from 'class-variance-authority' +import { forwardRef, HTMLAttributes } from 'react' + +import { cn } from 'ui' + +// ============================================================================ +// Variants +// ============================================================================ + +const pageContainerVariants = cva(['mx-auto w-full @container px-6 xl:px-10'], { + variants: { + size: { + small: 'max-w-[768px]', + default: 'max-w-[1200px]', + large: 'max-w-[1600px]', + full: 'max-w-none', + }, + }, + defaultVariants: { + size: 'default', + }, +}) + +// ============================================================================ +// Component +// ============================================================================ + +export type PageContainerProps = HTMLAttributes & + VariantProps + +/** + * Container component for page content. + * Provides consistent max-width and padding based on size prop. + * + * @example + * ```tsx + * + * + * My Page + * + * {children} + * + * ``` + */ +export const PageContainer = forwardRef( + ({ className, size, ...props }, ref) => { + return
+ } +) + +PageContainer.displayName = 'PageContainer' diff --git a/packages/ui-patterns/src/PageHeader/index.tsx b/packages/ui-patterns/src/PageHeader/index.tsx new file mode 100644 index 0000000000000..a9d624c4eefa9 --- /dev/null +++ b/packages/ui-patterns/src/PageHeader/index.tsx @@ -0,0 +1,282 @@ +import { cva, type VariantProps } from 'class-variance-authority' +import React from 'react' + +import { cn } from 'ui' +import { Breadcrumb } from 'ui/src/components/shadcn/ui/breadcrumb' +import { PageContainer } from '../PageContainer' + +// ============================================================================ +// Variants +// ============================================================================ + +const pageHeaderVariants = cva(['flex flex-col gap-4 w-full'], { + variants: { + size: { + default: 'pt-12', + small: 'pt-12', + large: 'pt-12', + full: 'pt-6', + }, + }, + defaultVariants: { + size: 'default', + }, +}) + +// ============================================================================ +// Root +// ============================================================================ + +export type PageHeaderRootProps = React.ComponentProps<'div'> & + VariantProps + +/** + * Root component for page header. + * Renders children in order without searching for specific components. + */ +const PageHeaderRoot = ({ + className, + size = 'default', + children, + ...props +}: PageHeaderRootProps) => { + return ( +
+ {children} +
+ ) +} + +// ============================================================================ +// Breadcrumb +// ============================================================================ + +export type PageHeaderBreadcrumbProps = React.ComponentProps & { + size?: 'default' | 'small' | 'large' | 'full' +} + +/** + * Breadcrumb component for page header. + * A wrapper around Breadcrumb with page header styling. + * Should be placed as the first child of PageHeader. + */ +const PageHeaderBreadcrumb = ({ + className, + size = 'default', + children, + ...props +}: PageHeaderBreadcrumbProps) => { + return ( + + + {children} + + + ) +} +PageHeaderBreadcrumb.displayName = 'PageHeaderBreadcrumb' + +// ============================================================================ +// Icon +// ============================================================================ + +export type PageHeaderIconProps = React.ComponentProps<'div'> + +/** + * Icon component for page header. + * Positioned to the left of title and description. + */ +const PageHeaderIcon = ({ className, ...props }: PageHeaderIconProps) => { + return ( +
+ ) +} +PageHeaderIcon.displayName = 'PageHeaderIcon' + +// ============================================================================ +// Summary +// ============================================================================ + +export type PageHeaderSummaryProps = React.ComponentProps<'div'> + +/** + * Summary component to contain title and description. + * Provides layout structure for text content. + * Should be placed inside PageHeaderMeta. + */ +const PageHeaderSummary = ({ className, children, ...props }: PageHeaderSummaryProps) => { + return ( +
+ {children} +
+ ) +} +PageHeaderSummary.displayName = 'PageHeaderSummary' + +// ============================================================================ +// Title +// ============================================================================ + +export type PageHeaderTitleProps = React.ComponentProps<'h1'> + +/** + * Title component for page header. + * Primary heading for the page. + */ +const PageHeaderTitle = ({ className, children, ...props }: PageHeaderTitleProps) => { + return ( +

+ {children} +

+ ) +} + +// ============================================================================ +// Description +// ============================================================================ + +export type PageHeaderDescriptionProps = React.ComponentProps<'div'> + +/** + * Description component for page header. + * Supporting text rendered below the title. + */ +const PageHeaderDescription = ({ className, children, ...props }: PageHeaderDescriptionProps) => { + return ( +
+ {children} +
+ ) +} + +// ============================================================================ +// Meta +// ============================================================================ + +export type PageHeaderMetaProps = React.ComponentProps<'div'> & { + size?: 'default' | 'small' | 'large' | 'full' +} + +/** + * Meta wrapper for page header. + * Contains icon, summary, and aside components with proper layout. + * Should be placed after PageHeaderBreadcrumb (if present) and before PageHeaderFooter. + * Uses CSS to style children based on their data-slot attributes. + */ +const PageHeaderMeta = ({ + className, + size = 'default', + children, + ...props +}: PageHeaderMetaProps) => { + return ( + +
[data-slot="page-header-icon"]]:shrink-0', + '[&>[data-slot="page-header-summary"]]:flex-1', + className + )} + {...props} + > + {children} +
+
+ ) +} +PageHeaderMeta.displayName = 'PageHeaderMeta' + +// ============================================================================ +// Actions +// ============================================================================ + +export type PageHeaderAsideProps = React.ComponentProps<'div'> + +/** + * Actions component for page header. + * Container for buttons and other action elements. + * Should be placed inside PageHeaderMeta. + */ +const PageHeaderAside = ({ className, ...props }: PageHeaderAsideProps) => { + return ( +
+ ) +} +PageHeaderAside.displayName = 'PageHeaderAside' + +// ============================================================================ +// Navigation +// ============================================================================ + +export type PageHeaderFooterProps = React.ComponentProps<'div'> + +/** + * Navigation component for page header. + * Container for tab navigation (NavMenu). + * Should be placed as the last child of PageHeader. + */ +const PageHeaderFooter = ({ + className, + size = 'default', + ...props +}: PageHeaderFooterProps & { size?: 'default' | 'small' | 'large' | 'full' }) => { + return ( + +
nav]:border-b-0', size !== 'full' && 'border-b', className)} + {...props} + /> + + ) +} +PageHeaderFooter.displayName = 'PageHeaderFooter' + +// ============================================================================ +// Exports +// ============================================================================ + +export type PageHeaderProps = PageHeaderRootProps + +/** + * Page header root component. + * Use PageHeader, PageHeaderBreadcrumb, PageHeaderMeta, PageHeaderIcon, etc. + */ +export { + PageHeaderRoot as PageHeader, + PageHeaderAside, + PageHeaderBreadcrumb, + PageHeaderDescription, + PageHeaderFooter, + PageHeaderIcon, + PageHeaderMeta, + PageHeaderSummary, + PageHeaderTitle, +} diff --git a/packages/ui-patterns/src/PageSection/index.tsx b/packages/ui-patterns/src/PageSection/index.tsx new file mode 100644 index 0000000000000..b13fc7c382974 --- /dev/null +++ b/packages/ui-patterns/src/PageSection/index.tsx @@ -0,0 +1,207 @@ +import { cva, type VariantProps } from 'class-variance-authority' +import React from 'react' + +import { cn } from 'ui' + +// ============================================================================ +// Variants +// ============================================================================ + +const pageSectionRootVariants = cva(['pt-12 last:pb-12 gap-6'], { + variants: { + orientation: { + horizontal: 'grid @3xl:grid-cols-[1fr_2fr] @3xl:gap-12', + vertical: 'flex flex-col', + }, + }, + defaultVariants: { + orientation: 'vertical', + }, +}) + +// ============================================================================ +// Root +// ============================================================================ + +export type PageSectionRootProps = React.ComponentProps<'div'> & + VariantProps + +/** + * Root component for page section. + * Provides layout structure for section content with orientation variants. + * Renders children in order without searching for specific components. + */ +const PageSectionRoot = ({ + className, + orientation = 'vertical', + children, + ...props +}: PageSectionRootProps) => { + return ( +
+ {children} +
+ ) +} + +PageSectionRoot.displayName = 'PageSectionRoot' + +// ============================================================================ +// Summary +// ============================================================================ + +export type PageSectionSummaryProps = React.ComponentProps<'div'> + +/** + * Summary component to contain title and description. + * Provides layout structure for text content. + * Should be placed inside PageSectionMeta. + */ +const PageSectionSummary = ({ className, children, ...props }: PageSectionSummaryProps) => { + return ( +
+ {children} +
+ ) +} +PageSectionSummary.displayName = 'PageSectionSummary' + +// ============================================================================ +// Title +// ============================================================================ + +export type PageSectionTitleProps = React.ComponentProps<'h2'> + +/** + * Title component for page section. + * Primary heading for the section. + */ +const PageSectionTitle = ({ className, children, ...props }: PageSectionTitleProps) => { + return ( +

+ {children} +

+ ) +} +PageSectionTitle.displayName = 'PageSectionTitle' + +// ============================================================================ +// Description +// ============================================================================ + +export type PageSectionDescriptionProps = React.ComponentProps<'div'> + +/** + * Description component for page section. + * Supporting text rendered below the title. + */ +const PageSectionDescription = ({ className, children, ...props }: PageSectionDescriptionProps) => { + return ( +
+ {children} +
+ ) +} +PageSectionDescription.displayName = 'PageSectionDescription' + +// ============================================================================ +// Aside +// ============================================================================ + +export type PageSectionAsideProps = React.ComponentProps<'div'> + +/** + * Aside component for page section. + * Container for buttons and other action elements. + * Should be placed inside PageSectionMeta. + */ +const PageSectionAside = ({ className, ...props }: PageSectionAsideProps) => { + return ( +
+ ) +} +PageSectionAside.displayName = 'PageSectionAside' + +// ============================================================================ +// Meta +// ============================================================================ + +export type PageSectionMetaProps = React.ComponentProps<'div'> + +/** + * Meta wrapper for page section. + * Contains summary and aside components with proper layout. + * Should be placed as the first child of PageSectionRoot. + * Uses CSS to style children based on their data-slot attributes. + */ +const PageSectionMeta = ({ className, children, ...props }: PageSectionMetaProps) => { + return ( +
+
[data-slot="page-section-summary"]]:flex-1', + '[&>[data-slot="page-section-aside"]]:shrink-0', + className + )} + {...props} + > + {children} +
+
+ ) +} +PageSectionMeta.displayName = 'PageSectionMeta' + +// ============================================================================ +// Content +// ============================================================================ + +export type PageSectionContentProps = React.ComponentProps<'div'> + +/** + * Content component for page section. + * Container for the main section content. + */ +const PageSectionContent = ({ className, ...props }: PageSectionContentProps) => { + return
+} +PageSectionContent.displayName = 'PageSectionContent' + +// ============================================================================ +// Exports +// ============================================================================ + +export type PageSectionProps = PageSectionRootProps + +/** + * Page section root component. + * Use PageSection, PageSectionMeta, PageSectionSummary, etc. + */ +export { + PageSectionRoot as PageSection, + PageSectionAside, + PageSectionContent, + PageSectionDescription, + PageSectionMeta, + PageSectionSummary, + PageSectionTitle, +} diff --git a/packages/ui/src/components/shadcn/ui/breadcrumb.tsx b/packages/ui/src/components/shadcn/ui/breadcrumb.tsx index e76be1861908e..062b60d367017 100644 --- a/packages/ui/src/components/shadcn/ui/breadcrumb.tsx +++ b/packages/ui/src/components/shadcn/ui/breadcrumb.tsx @@ -75,7 +75,7 @@ const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentP