Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/design-system/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,26 @@ Open [http://localhost:3003](http://localhost:3003) in your browser to see the r
### Watching for MDX changes

The `dev:full` command automatically watches for changes to MDX files with hot reload. If you're running the `pnpm dev` separately, you'll need to run `pnpm content:dev` in a separate terminal shell to watch for content changes.

### Adding components

The design system _references_ components rather than housing them. That’s an important distinction to make, as everything that follows here is about the documentation of components. You can add or edit components in one of these two places:

- [`packages/ui`](https://github.com/supabase/supabase/tree/master/packages/ui): basic UI components
- [`packages/ui-patterns`](https://github.com/supabase/supabase/tree/master/packages/ui-patterns): components which are built using NPM libraries or amalgamations of components from `patterns/ui`

With that out of the way, there are several parts of this design system that need to be manually updated after components have been added or removed (from documentation). These include:

- `config/docs.ts`: list of components in the sidebar
- `content/docs`: where the actual component documentation `.mdx` file lives
- `registry/examples.ts`: list of example components
- `registry/default/example`: where the actual example component(s) live
- `registry/charts.ts`: Chart components
- `registry/fragments.ts`: Fragment components

You will need to rebuild the design system’s registry each time a new example component is added. In other words: whenever a new file enters `registry`, it needs to be rebuilt. You can do that via:

```bash
cd apps/design-system
pnpm build:registry
```
44 changes: 44 additions & 0 deletions apps/design-system/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,50 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"data-input-demo": {
name: "data-input-demo",
type: "components:example",
registryDependencies: ["data-input"],
component: React.lazy(() => import("@/registry/default/example/data-input-demo")),
source: "",
files: ["registry/default/example/data-input-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"data-input-with-copy": {
name: "data-input-with-copy",
type: "components:example",
registryDependencies: ["data-input"],
component: React.lazy(() => import("@/registry/default/example/data-input-with-copy")),
source: "",
files: ["registry/default/example/data-input-with-copy.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"data-input-with-copy-secret": {
name: "data-input-with-copy-secret",
type: "components:example",
registryDependencies: ["data-input"],
component: React.lazy(() => import("@/registry/default/example/data-input-with-copy-secret")),
source: "",
files: ["registry/default/example/data-input-with-copy-secret.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"data-input-with-reveal-copy": {
name: "data-input-with-reveal-copy",
type: "components:example",
registryDependencies: ["data-input"],
component: React.lazy(() => import("@/registry/default/example/data-input-with-reveal-copy")),
source: "",
files: ["registry/default/example/data-input-with-reveal-copy.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"date-picker-demo": {
name: "date-picker-demo",
type: "components:example",
Expand Down
5 changes: 5 additions & 0 deletions apps/design-system/config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/fragments/inner-side-menu',
items: [],
},
{
title: 'Data Input',
href: '/docs/fragments/data-input',
items: [],
},
{
title: 'Form Item Layout',
href: '/docs/fragments/form-item-layout',
Expand Down
47 changes: 47 additions & 0 deletions apps/design-system/content/docs/fragments/data-input.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: Data Input
description: Set, read, or copy a value on a single line.
component: true
---

<ComponentPreview name="data-input-demo" peekCode wide />

Referred to as `Input` in code, not `DataInput`. Also not to be confused with the atomic [Input](../components/input) component.

## Usage

### Read-only values

Input should be used for read-only values that can be copied or otherwise interacted with, as the input element is both keyboard and mouse-friendly.

<ComponentPreview name="data-input-with-copy" peekCode wide />

### Sensitive values

Inputs with sensitive values can be both revealed _and_ copied, but only in succession. This reduces the amount of actions on screen, thus simplifying the interface.

<ComponentPreview name="data-input-with-reveal-copy" peekCode wide />

You can also partially truncate the value by overriding the placeholder value.

Consider if the value needs to be revealed in the first place, as only copying is sufficient in most cases.

A happy medium might be to display a partially masked value but saving the actual value in the clipboard. To do this, pass a pre-masked string as the `value` prop and override the `onCopy` callback to copy the real value.

<ComponentPreview name="data-input-with-copy-secret" peekCode wide />

### Password managers

In some cases, you may need to add the following attribute to your input to prevent password managers from applying their widgets and dropdowns to the input:

```jsx
<Input
readOnly
copy
value={name}
data-1p-ignore // 1Password
data-lpignore="true" // LastPass
data-form-type="other" // Dashlane
data-bwignore // Bitwarden
/>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Input } from 'ui-patterns/DataInputs/Input'

export default function DataInputDemo() {
return <Input containerClassName="w-full max-w-sm" placeholder="Hello world" />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Input } from 'ui-patterns/DataInputs/Input'

export default function DataInputWithCopySecret() {
const actualValue = 'sb_secret_1234567890'
const maskedValue = 'sb_secret_123•••••••'

return (
<Input
containerClassName="w-full max-w-sm"
readOnly
copy
value={maskedValue}
onCopy={() => {
navigator.clipboard.writeText(actualValue)
}}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Input } from 'ui-patterns/DataInputs/Input'

export default function DataInputWithCopy() {
return <Input containerClassName="w-full max-w-sm" readOnly copy value="1234567890" />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Input } from 'ui-patterns/DataInputs/Input'

export default function DataInputWithRevealCopy() {
return <Input containerClassName="w-full max-w-sm" readOnly reveal copy value="1234567890" />
}
30 changes: 24 additions & 6 deletions apps/design-system/registry/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,12 +385,30 @@ export const examples: Registry = [
registryDependencies: ['context-menu'],
files: ['example/context-menu-demo.tsx'],
},
// {
// name: 'data-table-demo',
// type: 'components:example',
// registryDependencies: ['data-table'],
// files: ['example/data-table-demo.tsx'],
// },
{
name: 'data-input-demo',
type: 'components:example',
registryDependencies: ['data-input'],
files: ['example/data-input-demo.tsx'],
},
{
name: 'data-input-with-copy',
type: 'components:example',
registryDependencies: ['data-input'],
files: ['example/data-input-with-copy.tsx'],
},
{
name: 'data-input-with-copy-secret',
type: 'components:example',
registryDependencies: ['data-input'],
files: ['example/data-input-with-copy-secret.tsx'],
},
{
name: 'data-input-with-reveal-copy',
type: 'components:example',
registryDependencies: ['data-input'],
files: ['example/data-input-with-reveal-copy.tsx'],
},
{
name: 'date-picker-demo',
type: 'components:example',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const ReplicationStaticMockup = ({ projectRef }: { projectRef: string }) => {
blank: BlankNode,
cta: () => CTANode({ projectRef }),
}),
[]
[projectRef]
)

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { UseFormReturn } from 'react-hook-form'

import {
Accordion_Shadcn_,
AccordionContent_Shadcn_,
AccordionItem_Shadcn_,
AccordionTrigger_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
Input_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { DestinationPanelSchemaType } from './DestinationPanel.schema'

export const AdvancedSettings = ({ form }: { form: UseFormReturn<DestinationPanelSchemaType> }) => {
const { type } = form.watch()

return (
<Accordion_Shadcn_ type="single" collapsible>
<AccordionItem_Shadcn_ value="item-1" className="border-none">
<AccordionTrigger_Shadcn_ className="font-normal gap-2 justify-between text-sm">
Advanced Settings
</AccordionTrigger_Shadcn_>
<AccordionContent_Shadcn_ asChild className="!pb-0">
<FormField_Shadcn_
control={form.control}
name="maxFillMs"
render={({ field }) => (
<FormItemLayout
className="mb-4"
label="Max fill milliseconds"
layout="vertical"
description="The maximum amount of time to fill the data in milliseconds. Leave empty to use default value."
>
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
type="number"
value={field.value ?? ''}
onChange={(e) => {
const val = e.target.value
field.onChange(val === '' ? undefined : Number(val))
}}
placeholder="Leave empty for default"
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
{type === 'BigQuery' && (
<FormField_Shadcn_
control={form.control}
name="maxStalenessMins"
render={({ field }) => (
<FormItemLayout
className="mb-4"
label="Max staleness minutes"
layout="vertical"
description="Maximum staleness time allowed in minutes. Leave empty to use default value."
>
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
type="number"
value={field.value ?? ''}
onChange={(e) => {
const val = e.target.value
field.onChange(val === '' ? undefined : Number(val))
}}
placeholder="Leave empty for default"
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
)}
</AccordionContent_Shadcn_>
</AccordionItem_Shadcn_>
</Accordion_Shadcn_>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Hardcoded value for `s3AccessKeyId` field in the form to indicate creating a new key
export const CREATE_NEW_KEY = 'create-new'

// Hardcoded value for `namespace` field in the form to indicate creating a new namespace
export const CREATE_NEW_NAMESPACE = 'create-new-namespace'
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as z from 'zod'

const types = ['BigQuery', 'Analytics Bucket'] as const
export const TypeEnum = z.enum(types)

// [Joshen] JFYI if we plan to add another type here, I reckon we split this out into smaller components
// then as FormSchema is getting quite complex with some fields that aren't necessary if the type is one or the other
export const DestinationPanelFormSchema = z
.object({
// Common fields
type: TypeEnum,
name: z.string().min(1, 'Name is required'),
publicationName: z.string().min(1, 'Publication is required'),
maxFillMs: z.number().min(1, 'Max Fill milliseconds should be greater than 0').int().optional(),
// BigQuery fields
projectId: z.string().optional(),
datasetId: z.string().optional(),
serviceAccountKey: z.string().optional(),
maxStalenessMins: z.number().nonnegative().optional(),
// Analytics Bucket fields, only warehouse name and namespace are visible + editable fields
warehouseName: z.string().optional(),
namespace: z.string().optional(),
newNamespaceName: z.string().optional(),
catalogToken: z.string().optional(),
s3AccessKeyId: z.string().optional(),
s3SecretAccessKey: z.string().optional(),
s3Region: z.string().optional(),
})
.refine(
(data) => {
if (data.type === 'BigQuery') {
return (
data.projectId &&
data.projectId.length > 0 &&
data.datasetId &&
data.datasetId.length > 0 &&
data.serviceAccountKey &&
data.serviceAccountKey.length > 0
)
} else if (data.type === 'Analytics Bucket') {
const hasValidNamespace =
(data.namespace && data.namespace.length > 0) ||
(data.namespace === 'create-new-namespace' &&
data.newNamespaceName &&
data.newNamespaceName.length > 0)

return (
data.warehouseName &&
data.warehouseName.length > 0 &&
hasValidNamespace &&
data.s3Region &&
data.s3Region.length > 0
)
}
return true
},
{
message: 'All fields are required for the selected destination type',
path: ['projectId'],
}
)

export type DestinationPanelSchemaType = z.infer<typeof DestinationPanelFormSchema>
Loading
Loading