diff --git a/.github/workflows/docs-lint-v2.yml b/.github/workflows/docs-lint-v2.yml index 6586eb38daf..aa977fcd5c4 100644 --- a/.github/workflows/docs-lint-v2.yml +++ b/.github/workflows/docs-lint-v2.yml @@ -63,7 +63,7 @@ jobs: env: BASE_REF: ${{ github.base_ref }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BRANCH_NAME: ${{ github.head_ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | set -o pipefail run_lints() { @@ -76,6 +76,6 @@ jobs: LINT_EXIT_CODE=$? set -e if [[ $LINT_EXIT_CODE -ne 0 ]]; then - gh pr comment "$BRANCH_NAME" --body "$LINT_RESULTS" + gh pr comment "$PR_NUMBER" --body "$LINT_RESULTS" exit 1 fi diff --git a/.gitignore b/.gitignore index b6ce235b350..e7cd51f7258 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,6 @@ gcloud.json # sitemaps # apps/www/public/*.xml # apps/docs/public/*.xml + +# CLI version file +.temp/cli-latest diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index d4554da3efa..71d36ee0000 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -720,6 +720,17 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "filter-bar-demo": { + name: "filter-bar-demo", + type: "components:example", + registryDependencies: ["filter-bar"], + component: React.lazy(() => import("@/registry/default/example/filter-bar-demo")), + source: "", + files: ["registry/default/example/filter-bar-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "hover-card-demo": { name: "hover-card-demo", type: "components:example", diff --git a/apps/design-system/config/docs.ts b/apps/design-system/config/docs.ts index 1031738338e..e3bda291b96 100644 --- a/apps/design-system/config/docs.ts +++ b/apps/design-system/config/docs.ts @@ -117,6 +117,11 @@ export const docsConfig: DocsConfig = { href: '/docs/fragments/multi-select', items: [], }, + { + title: 'Filter Bar', + href: '/docs/fragments/filter-bar', + items: [], + }, { title: 'Logs Bar Chart', href: '/docs/fragments/logs-bar-chart', diff --git a/apps/design-system/content/docs/fragments/filter-bar.mdx b/apps/design-system/content/docs/fragments/filter-bar.mdx new file mode 100644 index 00000000000..fed27c7219d --- /dev/null +++ b/apps/design-system/content/docs/fragments/filter-bar.mdx @@ -0,0 +1,214 @@ +--- +title: Filter Bar +description: An advanced filtering component with support for multiple conditions and operators. +component: true +fragment: true +--- + + + +## Usage + +The Filter Bar component provides advanced filtering capabilities with support for multiple conditions, operators, and different field types. It can be used with both static and async options. + +```tsx +const filterProperties = [ + { + label: 'Name', + name: 'name', + type: 'string', + operators: ['=', '!=', 'CONTAINS', 'STARTS WITH', 'ENDS WITH'], + }, + { + label: 'Status', + name: 'status', + type: 'string', + options: ['active', 'inactive', 'pending'], + operators: ['=', '!='], + }, +] + +export function FilterDemo() { + const [filters, setFilters] = useState(initialFilters) + const [freeformText, setFreeformText] = useState('') + + return ( + + ) +} +``` + +## API Reference + +### FilterProperty + +```tsx +interface FilterProperty { + label: string + name: string + type: 'string' | 'number' | 'date' | 'boolean' + operators: string[] + options?: string[] | ((search?: string) => Promise | string[]) +} +``` + +### FilterGroup + +```tsx +interface FilterGroup { + logicalOperator: 'AND' | 'OR' + conditions: (FilterCondition | FilterGroup)[] +} +``` + +### FilterCondition + +```tsx +interface FilterCondition { + propertyName: string + value: string | number | boolean | Date + operator: string +} +``` + +### Component Props + +| Prop | Type | Description | +| -------------------- | ------------------------------ | ---------------------------------------- | +| filterProperties | FilterProperty[] | Array of properties that can be filtered | +| filters | FilterGroup | Current filter state | +| onFilterChange | (filters: FilterGroup) => void | Callback when filters change | +| freeformText | string | Current free-form search text | +| onFreeformTextChange | (text: string) => void | Callback when free-form text changes | +| aiApiUrl | string? | Optional URL for AI-powered filtering | + +## AI Integration + +The Filter Bar component supports AI-powered filtering through an optional API endpoint. When `aiApiUrl` is provided, the component will send natural language queries to be converted into structured filters. + +### API Endpoint + +The AI API endpoint should accept POST requests with the following structure: + +```typescript +// Request body +interface AIFilterRequest { + prompt: string // Natural language query + filterProperties: FilterProperty[] // Available filter properties +} + +// Response body +interface AIFilterResponse { + logicalOperator: 'AND' | 'OR' + conditions: (FilterCondition | FilterGroup)[] +} +``` + +### Example API Implementation + +```typescript +import { generateObject } from 'ai' +import { openai } from '@ai-sdk/openai' +import { z } from 'zod' + +// Define schemas for validation +const FilterProperty = z.object({ + label: z.string(), + name: z.string(), + type: z.enum(['string', 'number', 'date', 'boolean']), + options: z.array(z.string()).optional(), + operators: z.array(z.string()).optional(), +}) + +const FilterCondition = z.object({ + propertyName: z.string(), + value: z.union([z.string(), z.number(), z.boolean(), z.null()]), + operator: z.string(), +}) + +type FilterGroupType = { + logicalOperator: 'AND' | 'OR' + conditions: Array | FilterGroupType> +} + +const FilterGroup: z.ZodType = z.lazy(() => + z.object({ + logicalOperator: z.enum(['AND', 'OR']), + conditions: z.array(z.union([FilterCondition, FilterGroup])), + }) +) + +export async function POST(req: Request) { + const { prompt, filterProperties } = await req.json() + const filterPropertiesString = JSON.stringify(filterProperties) + + try { + const { object } = await generateObject({ + model: openai('gpt-4-mini'), + schema: FilterGroup, + prompt: `Generate a filter group based on the following prompt: "${prompt}". + Use only these filter properties: ${filterPropertiesString}. + Each property has its own set of valid operators defined in the operators field. + Return a filter group with a logical operator ('AND'/'OR') and an array of conditions. + Each condition can be either a filter condition or another filter group. + Filter conditions should have the structure: { propertyName: string, value: string | number | boolean | null, operator: string }. + Ensure that the generated filters use only the provided property names and their corresponding operators.`, + }) + + // Validate that all propertyNames exist in filterProperties + const validatePropertyNames = (group: FilterGroupType): boolean => { + return group.conditions.every((condition) => { + if ('logicalOperator' in condition) { + return validatePropertyNames(condition as FilterGroupType) + } + const property = filterProperties.find( + (p: z.infer) => p.name === condition.propertyName + ) + if (!property) return false + // Validate operator is valid for this property + return property.operators?.includes(condition.operator) ?? false + }) + } + + if (!validatePropertyNames(object)) { + throw new Error('Invalid property names or operators in generated filter') + } + + // Zod will throw an error if the object doesn't match the schema + const validatedFilters = FilterGroup.parse(object) + return Response.json(validatedFilters) + } catch (error: any) { + console.error('Error in AI filtering:', error) + return Response.json({ error: error.message || 'AI filtering failed' }, { status: 500 }) + } +} +``` + +### Usage with AI + +```tsx +export function FilterDemoWithAI() { + const [filters, setFilters] = useState(initialFilters) + + return ( + + ) +} +``` diff --git a/apps/design-system/public/llms.txt b/apps/design-system/public/llms.txt index 6d50fe9e7f0..34725799b8c 100644 --- a/apps/design-system/public/llms.txt +++ b/apps/design-system/public/llms.txt @@ -1,5 +1,5 @@ # Supabase Design System -Last updated: 2025-03-05T12:59:50.911Z +Last updated: 2025-04-24T06:00:07.374Z ## Overview The Design System used in Supabase Studio. @@ -130,6 +130,8 @@ The Design System used in Supabase Studio. - Displays a callout for user attention. - [Assistant Chat](https://supabase-design-system.vercel.app/design-system/docs/fragments/assistant-chat) - Bespoke Assistant chat text area, with support for a commands Popover. +- [Filter Bar](https://supabase-design-system.vercel.app/design-system/docs/fragments/filter-bar) + - An advanced filtering component with support for multiple conditions and operators. - [Form Item Layout](https://supabase-design-system.vercel.app/design-system/docs/fragments/form-item-layout) - A helper component that provides a layout for form items. - [Info Tooltip](https://supabase-design-system.vercel.app/design-system/docs/fragments/info-tooltip) @@ -144,6 +146,8 @@ The Design System used in Supabase Studio. - Multiple selection component. - [Text Confirm Dialog](https://supabase-design-system.vercel.app/design-system/docs/fragments/text-confirm-dialog) - A modal dialog that interrupts the user with important content and expects a response. +- [Table of Contents (TOC)](https://supabase-design-system.vercel.app/design-system/docs/fragments/toc) + - List of page anchors for the current page. - [Icons](https://supabase-design-system.vercel.app/design-system/docs/icons) - Icons system breakdown. Copy values of Icons. - [Introduction](https://supabase-design-system.vercel.app/design-system/docs/index) diff --git a/apps/design-system/registry/default/example/filter-bar-demo.tsx b/apps/design-system/registry/default/example/filter-bar-demo.tsx new file mode 100644 index 00000000000..06ccabdc71e --- /dev/null +++ b/apps/design-system/registry/default/example/filter-bar-demo.tsx @@ -0,0 +1,166 @@ +import { format } from 'date-fns' +import { useState } from 'react' +import { Button, Button_Shadcn_, Calendar, Input_Shadcn_ } from 'ui' +import { CustomOptionProps, FilterBar, FilterGroup } from 'ui-patterns' + +function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) { + const [date, setDate] = useState( + search + ? { + from: new Date(search), + to: undefined, + } + : undefined + ) + + return ( +
+ +
+ + +
+
+ ) +} + +function CustomTimePicker({ onChange, onCancel, search }: CustomOptionProps) { + const [time, setTime] = useState(search || '') + + return ( +
+

Select Time

+ setTime(e.target.value)} /> +
+ + Cancel + + onChange(time)}>Apply +
+
+ ) +} + +function CustomRangePicker({ onChange, onCancel, search }: CustomOptionProps) { + const [range, setRange] = useState(search || '') + + return ( +
+

Select Range

+ setRange(e.target.value)} /> +
+ + Cancel + + onChange(range)}>Apply +
+
+ ) +} + +const filterProperties = [ + { + label: 'Name', + name: 'name', + type: 'string' as const, + operators: ['=', '!=', 'CONTAINS', 'STARTS WITH', 'ENDS WITH'], + }, + { + label: 'Status', + name: 'status', + type: 'string' as const, + options: [ + { label: 'Active', value: 'active' }, + { label: 'Inactive', value: 'inactive' }, + { label: 'Pending', value: 'pending' }, + ], + operators: ['=', '!='], + }, + { + label: 'Type', + name: 'type', + type: 'string' as const, + options: async (search?: string) => { + await new Promise((resolve) => setTimeout(resolve, 500)) + const allOptions = ['user', 'admin', 'guest'] + return search + ? allOptions.filter((option) => option.toLowerCase().includes(search.toLowerCase())) + : allOptions + }, + operators: ['=', '!='], + }, + { + label: 'Time period', + name: 'created_at', + type: 'date' as const, + options: [ + { label: 'Today', value: format(new Date(), 'yyyy-MM-dd') }, + { label: 'Yesterday', value: format(new Date(Date.now() - 86400000), 'yyyy-MM-dd') }, + { label: 'Last 7 days', value: format(new Date(Date.now() - 7 * 86400000), 'yyyy-MM-dd') }, + { label: 'Last 30 days', value: format(new Date(Date.now() - 30 * 86400000), 'yyyy-MM-dd') }, + { + label: 'Pick a date...', + component: (props: CustomOptionProps) => , + }, + ], + operators: ['=', '!=', '>', '<', '>=', '<='], + }, + { + label: 'Priority', + name: 'priority', + type: 'number' as const, + options: { + component: (props: CustomOptionProps) => ( +
+ +
+ ), + }, + triggerOnPropertyClick: true, + operators: ['=', '!=', '>', '<', '>=', '<='], + }, +] + +const initialFilters: FilterGroup = { + logicalOperator: 'AND', + conditions: [], +} + +export default function FilterBarDemo() { + const [filters, setFilters] = useState(initialFilters) + const [freeformText, setFreeformText] = useState('') + + return ( +
+ +
+ ) +} diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index 2198f2527af..b79b6322d44 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -467,6 +467,12 @@ export const examples: Registry = [ registryDependencies: ['dropdown-menu', 'radio-group'], files: ['example/dropdown-menu-radio-group.tsx'], }, + { + name: 'filter-bar-demo', + type: 'components:example', + registryDependencies: ['filter-bar'], + files: ['example/filter-bar-demo.tsx'], + }, { name: 'hover-card-demo', type: 'components:example', diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore index 6eeb1dafe26..157f35a66e2 100644 --- a/apps/docs/.gitignore +++ b/apps/docs/.gitignore @@ -20,6 +20,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +**/__generated__/**/* **/*/generated/**/* !**/*/generated/.gitkeep !**/*/generated/**/.gitkeep diff --git a/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap b/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap new file mode 100644 index 00000000000..92aa9a51c06 --- /dev/null +++ b/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap @@ -0,0 +1,12 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`/api/graphql schema snapshot > should match snapshot 1`] = ` +"schema { + query: RootQueryType +} + +type RootQueryType { + """Get the GraphQL schema for this endpoint""" + schema: String! +}" +`; diff --git a/apps/docs/app/api/graphql/route.test.ts b/apps/docs/app/api/graphql/route.test.ts new file mode 100644 index 00000000000..f77a823659e --- /dev/null +++ b/apps/docs/app/api/graphql/route.test.ts @@ -0,0 +1,84 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { POST } from './route' + +describe('/api/graphql basic error statuses', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.spyOn(console, 'error').mockImplementation(() => {}) + }) + + it('should return error if request body is not valid JSON', async () => { + const request = new Request('http://localhost/api/graphql', { + method: 'POST', + body: 'not json', + }) + + const response = await POST(request) + const json = await response.json() + expect(json.errors[0].message).toBe('Invalid request: Request body must be valid JSON') + }) + + it('should return error if request body is missing required fields', async () => { + const request = new Request('http://localhost/api/graphql', { + method: 'POST', + body: JSON.stringify({ variables: {} }), + }) + + const response = await POST(request) + const json = await response.json() + expect(json.errors[0].message).toContain( + 'Invalid request: Request body must be valid GraphQL request object' + ) + }) + + it('should return error if query is not a string', async () => { + const request = new Request('http://localhost/api/graphql', { + method: 'POST', + body: JSON.stringify({ query: 123 }), + }) + + const response = await POST(request) + const json = await response.json() + expect(json.errors[0].message).toContain( + 'Invalid request: Request body must be valid GraphQL request object' + ) + }) + + it('should return error for internal server errors', async () => { + const request = new Request('http://localhost/api/graphql', { + method: 'POST', + body: JSON.stringify({ query: 'query { hello }' }), + }) + + vi.spyOn(request, 'json').mockImplementation(() => { + throw new Error('Unexpected error') + }) + + const response = await POST(request) + const json = await response.json() + expect(json.errors[0].message).toBe('Internal Server Error') + }) +}) + +describe('/api/graphql schema snapshot', () => { + it('should match snapshot', async () => { + const schemaQuery = ` + query { + schema + } + ` + const request = new Request('http://localhost/api/graphql', { + method: 'POST', + body: JSON.stringify({ query: schemaQuery }), + }) + + const response = await POST(request) + const json = await response.json() + expect(json.errors).toBeUndefined() + + const { + data: { schema }, + } = json + expect(schema).toMatchSnapshot() + }) +}) diff --git a/apps/docs/app/api/graphql/route.ts b/apps/docs/app/api/graphql/route.ts new file mode 100644 index 00000000000..c786b1c92fa --- /dev/null +++ b/apps/docs/app/api/graphql/route.ts @@ -0,0 +1,105 @@ +import { type DocumentNode, graphql, GraphQLError, parse, specifiedRules, validate } from 'graphql' +import { createComplexityLimitRule } from 'graphql-validation-complexity' +import { NextResponse } from 'next/server' +import { z } from 'zod' +import { ApiError, convertZodToInvalidRequestError, InvalidRequestError } from '~/app/api/utils' +import { BASE_PATH, IS_DEV } from '~/lib/constants' +import { rootGraphQLSchema } from '~/resources/rootSchema' +import { createQueryDepthLimiter } from './validators' + +export const runtime = 'edge' + +const MAX_DEPTH = 9 + +const validationRules = [ + ...specifiedRules, + createQueryDepthLimiter(MAX_DEPTH), + createComplexityLimitRule(1500, { + scalarCost: 1, + objectCost: 2, + listFactor: 10, + }), +] + +function isDevGraphiQL(request: Request) { + const origin = request.headers.get('Origin') + const referrer = request.headers.get('Referer') + return ( + IS_DEV && + origin.startsWith('http://localhost') && + referrer === `${origin}${BASE_PATH ?? ''}/graphiql` + ) +} + +const graphQLRequestSchema = z.object({ + query: z.string(), + variables: z.record(z.any()).optional(), + operationName: z.string().optional(), +}) + +async function handleGraphQLRequest(request: Request): Promise { + const body = await request.json().catch((error) => { + throw new InvalidRequestError('Request body must be valid JSON', error) + }) + const parsedBody = graphQLRequestSchema.safeParse(body) + if (!parsedBody.success) { + throw convertZodToInvalidRequestError( + parsedBody.error, + 'Request body must be valid GraphQL request object' + ) + } + + const { query, variables, operationName } = parsedBody.data + const validationErrors = validateGraphQLRequest(query, isDevGraphiQL(request)) + if (validationErrors.length > 0) { + return NextResponse.json({ + errors: validationErrors.map((error) => ({ + message: error.message, + locations: error.locations, + path: error.path, + })), + }) + } + + const result = await graphql({ + schema: rootGraphQLSchema, + contextValue: { request }, + source: query, + variableValues: variables, + operationName, + }) + return NextResponse.json(result) +} + +function validateGraphQLRequest(query: string, isDevGraphiQL = false): ReadonlyArray { + let documentAST: DocumentNode + try { + documentAST = parse(query) + } catch (error: unknown) { + if (error instanceof GraphQLError) { + return [error] + } else { + throw error + } + } + const rules = isDevGraphiQL ? specifiedRules : validationRules + return validate(rootGraphQLSchema, documentAST, rules) +} + +export async function POST(request: Request): Promise { + try { + return await handleGraphQLRequest(request) + } catch (error: unknown) { + console.error(error) + + if (error instanceof ApiError) { + return NextResponse.json({ + errors: [{ message: error.isPrivate() ? 'Internal Server Error' : error.message }], + }) + } else { + return NextResponse.json({ + errors: [{ message: 'Internal Server Error' }], + }) + } + } +} diff --git a/apps/docs/app/api/graphql/validators.test.ts b/apps/docs/app/api/graphql/validators.test.ts new file mode 100644 index 00000000000..0659623848d --- /dev/null +++ b/apps/docs/app/api/graphql/validators.test.ts @@ -0,0 +1,247 @@ +import { buildSchema, parse, validate } from 'graphql' +import { describe, expect, test } from 'vitest' +import { createQueryDepthLimiter } from './validators' + +describe('createQueryDepthLimiter', () => { + // Create a test schema + const schema = buildSchema(` + type Post { + id: ID! + title: String + content: String + author: User + comments: [Comment] + } + + type User { + id: ID! + name: String + email: String + posts: [Post] + friends: [User] + } + + type Comment { + id: ID! + text: String + author: User + post: Post + replies: [Comment] + } + + type Query { + post(id: ID!): Post + user(id: ID!): User + posts: [Post] + users: [User] + } + `) + + test('should not report error for queries under max depth', () => { + const query = ` + query { + posts { + title + author { + name + } + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors).toHaveLength(0) + }) + + test('should report error for queries exceeding max depth', () => { + const query = ` + query { + posts { + title + author { + name + posts { + title + } + } + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].message).toContain('Query exceeds maximum depth of 3') + }) + + test('should ignore __typename fields when calculating depth', () => { + const query = ` + query { + posts { + __typename + title + author { + __typename + name + posts { + __typename + } + } + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors).toHaveLength(0) + }) + + test('should handle fragments correctly', () => { + const query = ` + fragment UserDetails on User { + id + name + posts { + title + } + } + + query { + users { + ...UserDetails + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors).toHaveLength(0) + }) + + test('should report error for fragments exceeding max depth', () => { + const query = ` + fragment UserDetails on User { + id + name + posts { + title + author { + name + } + } + } + + query { + users { + ...UserDetails + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].message).toContain( + 'Fragment spread "UserDetails" causes query to exceed maximum depth' + ) + }) + + test('should report error for fragment spreads causing excessive depth', () => { + const query = ` + fragment PostDetails on Post { + title + content + } + + fragment UserDetails on User { + name + posts { + ...PostDetails + } + } + + query { + users { + friends { + ...UserDetails + } + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors.length).toBeGreaterThan(0) + }) + + test('should handle nested fragments correctly', () => { + const query = ` + fragment CommentDetails on Comment { + text + author { + name + } + } + + fragment PostDetails on Post { + title + comments { + ...CommentDetails + } + } + + query { + posts { + ...PostDetails + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors.length).toBeGreaterThan(0) + }) + + test('should handle inline fragments', () => { + const query = ` + query { + posts { + ... on Post { + title + author { + name + } + } + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors).toHaveLength(0) + }) + + test('should detect circular fragment references', () => { + const query = ` + fragment UserPosts on User { + posts { + author { + ...UserPosts + } + } + } + + query { + users { + ...UserPosts + } + } + ` + const queryAST = parse(query) + const maxDepth = 5 + // This should not go into infinite recursion + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors.length).toBeGreaterThan(0) + }) +}) diff --git a/apps/docs/app/api/graphql/validators.ts b/apps/docs/app/api/graphql/validators.ts new file mode 100644 index 00000000000..f4ca8e68df9 --- /dev/null +++ b/apps/docs/app/api/graphql/validators.ts @@ -0,0 +1,157 @@ +import { + type FragmentDefinitionNode, + GraphQLError, + type SelectionNode, + type ValidationContext, + type ValidationRule, +} from 'graphql' + +/** + * Creates a validator that limits the depth of a GraphQL query. + * + * @param maxDepth The maximum allowed depth + * @returns A validation rule that enforces depth limits + */ +export function createQueryDepthLimiter(maxDepth: number): ValidationRule { + const fragmentDepthMap = new Map() + + return function limitQueryDepth(context) { + return { + Document: { + leave() { + fragmentDepthMap.clear() + }, + }, + Field: { + enter(node, _key, _parent, path, ancestors) { + // Skip __typename and introspection fields + if (node.name.value === '__typename' || node.name.value.startsWith('__')) { + return + } + + // Calculate the depth by counting the ancestors that are field nodes + const fieldAncestors = ancestors.filter( + (ancestor) => ancestor && 'kind' in ancestor && ancestor.kind === 'Field' + ) + const depth = fieldAncestors.length + 1 + + // Check if this field exceeds the maximum maxDepth + if (depth > maxDepth) { + const pathStr = path.join('.') + context.reportError( + new GraphQLError( + `Query exceeds maximum depth of ${maxDepth}. Got depth ${depth} for ${pathStr}.`, + { nodes: node } + ) + ) + } + }, + }, + FragmentDefinition: { + enter(node) { + calculateFragmentDepth(node, context, 0, fragmentDepthMap, maxDepth) + const depth = fragmentDepthMap.get(node.name.value)! + + if (depth > maxDepth) { + context.reportError( + new GraphQLError( + `Fragment "${node.name.value}" exceeds maximum depth of ${maxDepth}.`, + { nodes: node } + ) + ) + } + }, + }, + FragmentSpread: { + enter(node, _key, _parent, _path, ancestors) { + const fragmentName = node.name.value + const fragmentDepth = fragmentDepthMap.get(fragmentName) || 0 + + const fieldAncestors = ancestors.filter( + (ancestor) => ancestor && 'kind' in ancestor && ancestor.kind === 'Field' + ) + const currentDepth = fieldAncestors.length + + const totalDepth = currentDepth + fragmentDepth + + if (totalDepth > maxDepth) { + context.reportError( + new GraphQLError( + `Fragment spread "${fragmentName}" causes query to exceed maximum depth of ${maxDepth}.`, + { nodes: node } + ) + ) + } + }, + }, + } + } +} + +/** + * Helper function to calculate the depth of a fragment + */ +function calculateFragmentDepth( + node: FragmentDefinitionNode, + context: ValidationContext, + currentDepth: number, + visitedFragments: Map, + maxDepth: number +) { + const fragmentName = node.name.value + if (visitedFragments.has(fragmentName)) { + return + } + + let maxFragmentDepth = 0 + // Process all selections in the fragment + function processSelections(selections: ReadonlyArray, depth: number) { + for (const selection of selections) { + if (selection.kind === 'Field') { + // Skip __typename and introspection fields + if (selection.name.value === '__typename' || selection.name.value.startsWith('__')) { + continue + } + + const newDepth = depth + 1 + if (newDepth > maxFragmentDepth) { + maxFragmentDepth = newDepth + } + if (maxFragmentDepth > maxDepth) { + // Do not recurse anymore because we might go on indefinitely + continue + } + + // If there are nested selections, process them + if (selection.selectionSet) { + processSelections(selection.selectionSet.selections, newDepth) + } + } else if (selection.kind === 'FragmentSpread') { + // Process fragment spreads + const spreadName = selection.name.value + const fragment = context.getFragment(spreadName) + if (!fragment) continue + + const fragmentName = fragment.name.value + if (!visitedFragments.has(fragmentName)) { + calculateFragmentDepth(fragment, context, depth, visitedFragments, maxDepth) + } + const fragmentDepth = visitedFragments.get(fragmentName)! + const totalDepth = fragmentDepth + depth + + if (totalDepth > maxFragmentDepth) { + maxFragmentDepth = totalDepth + } + } else if (selection.kind === 'InlineFragment' && selection.selectionSet) { + // Process inline fragments + processSelections(selection.selectionSet.selections, depth) + } + } + } + + if (node.selectionSet) { + processSelections(node.selectionSet.selections, currentDepth) + } + + visitedFragments.set(fragmentName, maxFragmentDepth) +} diff --git a/apps/docs/app/api/revalidate/route.test.ts b/apps/docs/app/api/revalidate/route.test.ts index befeabd43e7..66a30304d41 100644 --- a/apps/docs/app/api/revalidate/route.test.ts +++ b/apps/docs/app/api/revalidate/route.test.ts @@ -40,6 +40,9 @@ describe('_handleRevalidateRequest', () => { process.env.NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:3000' process.env.SUPABASE_SECRET_KEY = 'secret_key' + // Silence intentional console errors for cleaner test output + vi.spyOn(console, 'error').mockImplementation(() => {}) + // Mock current date mockDate = new Date('2023-01-01T12:00:00Z') vi.setSystemTime(mockDate) @@ -109,7 +112,7 @@ describe('_handleRevalidateRequest', () => { headers: { Authorization: 'Bearer basic_key', }, - body: JSON.stringify({ tags: ['tag1', 'tag2'] }), + body: JSON.stringify({ tags: ['graphql', 'wrappers'] }), }) vi.mocked(headers).mockReturnValue(new Headers(request.headers)) @@ -119,8 +122,8 @@ describe('_handleRevalidateRequest', () => { const response = await _handleRevalidateRequest(request) expect(response.status).toBe(204) expect(revalidateTag).toHaveBeenCalledTimes(2) - expect(revalidateTag).toHaveBeenCalledWith('tag1') - expect(revalidateTag).toHaveBeenCalledWith('tag2') + expect(revalidateTag).toHaveBeenCalledWith('graphql') + expect(revalidateTag).toHaveBeenCalledWith('wrappers') }) it('should return 429 if last revalidation was less than 6 hours ago with basic permissions', async () => { @@ -129,7 +132,7 @@ describe('_handleRevalidateRequest', () => { headers: { Authorization: 'Bearer basic_key', }, - body: JSON.stringify({ tags: ['tag1'] }), + body: JSON.stringify({ tags: ['graphql'] }), }) vi.mocked(headers).mockReturnValue(new Headers(request.headers)) @@ -150,7 +153,7 @@ describe('_handleRevalidateRequest', () => { headers: { Authorization: 'Bearer basic_key', }, - body: JSON.stringify({ tags: ['tag1'] }), + body: JSON.stringify({ tags: ['graphql'] }), }) vi.mocked(headers).mockReturnValue(new Headers(request.headers)) @@ -162,7 +165,7 @@ describe('_handleRevalidateRequest', () => { const response = await _handleRevalidateRequest(request) expect(response.status).toBe(204) - expect(revalidateTag).toHaveBeenCalledWith('tag1') + expect(revalidateTag).toHaveBeenCalledWith('graphql') }) it('should revalidate regardless of last revalidation time with override permissions', async () => { @@ -171,7 +174,7 @@ describe('_handleRevalidateRequest', () => { headers: { Authorization: 'Bearer override_key', }, - body: JSON.stringify({ tags: ['tag1'] }), + body: JSON.stringify({ tags: ['graphql'] }), }) vi.mocked(headers).mockReturnValue(new Headers(request.headers)) @@ -183,6 +186,6 @@ describe('_handleRevalidateRequest', () => { const response = await _handleRevalidateRequest(request) expect(response.status).toBe(204) - expect(revalidateTag).toHaveBeenCalledWith('tag1') + expect(revalidateTag).toHaveBeenCalledWith('graphql') }) }) diff --git a/apps/docs/app/api/revalidate/route.ts b/apps/docs/app/api/revalidate/route.ts index 1c751fb6512..86b033e4767 100644 --- a/apps/docs/app/api/revalidate/route.ts +++ b/apps/docs/app/api/revalidate/route.ts @@ -1,10 +1,10 @@ import { createClient } from '@supabase/supabase-js' +import { type Database } from 'common' import { revalidateTag } from 'next/cache' import { headers } from 'next/headers' import { type NextRequest } from 'next/server' import { z } from 'zod' - -import { type Database } from 'common' +import { VALID_REVALIDATION_TAGS } from '~/features/helpers.fetch' enum AuthorizationLevel { Unauthorized, @@ -13,7 +13,7 @@ enum AuthorizationLevel { } const requestBodySchema = z.object({ - tags: z.array(z.string()), + tags: z.array(z.enum(VALID_REVALIDATION_TAGS)), }) export const POST = handleError(_handleRevalidateRequest) @@ -35,14 +35,12 @@ export async function _handleRevalidateRequest(request: NextRequest) { } let authorizationLevel = AuthorizationLevel.Unauthorized - - const token = authorization.replace(/^Bearer\s+/, '') + const token = authorization.replace(/^Bearer /, '') if (overrideKeys.includes(token)) { authorizationLevel = AuthorizationLevel.Override } else if (basicKeys.includes(token)) { authorizationLevel = AuthorizationLevel.Basic } - if (authorizationLevel === AuthorizationLevel.Unauthorized) { return new Response('Invalid Authorization header', { status: 401 }) } diff --git a/apps/docs/app/api/utils.ts b/apps/docs/app/api/utils.ts new file mode 100644 index 00000000000..12e23bb605e --- /dev/null +++ b/apps/docs/app/api/utils.ts @@ -0,0 +1,40 @@ +import { z, type ZodError } from 'zod' + +export class ApiError extends Error { + constructor(message: string) { + super(message) + } + + isPrivate() { + return true + } + + statusCode() { + return 500 + } +} + +export class InvalidRequestError extends ApiError { + constructor(message: string, source?: unknown) { + super(`Invalid request: ${message}`) + } + + isPrivate() { + return false + } + + statusCode() { + return 400 + } +} + +export function convertZodToInvalidRequestError( + error: ZodError, + prelude?: string +): InvalidRequestError { + const issue = error.issues[0] + const pathStr = issue.path.join('.') + const message = `${prelude ? `${prelude}: ` : ''}${issue.message} at key "${pathStr}"` + + return new InvalidRequestError(message, error) +} diff --git a/apps/docs/app/graphiql/page.tsx b/apps/docs/app/graphiql/page.tsx new file mode 100644 index 00000000000..6443554bbbd --- /dev/null +++ b/apps/docs/app/graphiql/page.tsx @@ -0,0 +1,33 @@ +'use client' + +import dynamic from 'next/dynamic' +import { notFound } from 'next/navigation' +import { type ComponentProps } from 'react' +import { IS_DEV } from '~/lib/constants' + +const LazyGraphiQL = dynamic( + async () => { + const { GraphiQL: GraphiQLPrimitive } = await import('graphiql') + const { createGraphiQLFetcher } = await import('@graphiql/toolkit') + await import('graphiql/graphiql.css') + + return function GraphiQL(props: Omit, 'fetcher'>) { + const fetcher = createGraphiQLFetcher({ + url: 'http://localhost:3001/docs/api/graphql', + }) + return + } + }, + { + loading: () =>
Loading GraphiQL...
, + ssr: false, + } +) + +export default function GraphiQLPage() { + if (!IS_DEV) { + notFound() + } + + return +} diff --git a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx index 76c8c71bf48..8028d6c5625 100644 --- a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx @@ -4,29 +4,97 @@ import { readFile } from 'node:fs/promises' import { join, relative } from 'node:path' import rehypeSlug from 'rehype-slug' import emoji from 'remark-emoji' - +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, genGuidesStaticParams, removeRedundantH1, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' -import { fetchRevalidatePerDay } from '~/features/helpers.fetch' +import { REVALIDATION_TAGS } from '~/features/helpers.fetch' import { GUIDES_DIRECTORY, isValidGuideFrontmatter } from '~/lib/docs' import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' +import { octokit } from '~/lib/octokit' export const dynamicParams = false // We fetch these docs at build time from an external repo const org = 'supabase' const repo = 'wrappers' -const branch = 'main' const docsDir = 'docs/catalog' const externalSite = 'https://supabase.github.io/wrappers' +type TagQueryResponse = { + repository: { + refs: { + nodes: + | { + name: string + }[] + | null + pageInfo: { + hasNextPage: boolean + endCursor: string | null + } + } + } +} + +const tagQuery = ` + query TagQuery($owner: String!, $name: String!, $after: String) { + repository(owner: $owner, name: $name) { + refs( + refPrefix: "refs/tags/", + orderBy: { + field: TAG_COMMIT_DATE, + direction: DESC + }, + first: 5, + after: $after + ) { + nodes { + name + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + ` + +async function getLatestRelease(after: string | null = null) { + try { + const { + repository: { + refs: { + nodes, + pageInfo: { hasNextPage, endCursor }, + }, + }, + } = await octokit().graphql(tagQuery, { + owner: org, + name: repo, + after, + request: { + fetch: (url: RequestInfo | URL, options?: RequestInit) => + fetch(url, { ...options, next: { tags: [REVALIDATION_TAGS.WRAPPERS] } }), + }, + }) + + return ( + nodes?.find((node) => node?.name?.match(/^docs_v\d+\.\d+\.\d+/))?.name ?? + (hasNextPage && endCursor ? await getLatestRelease(endCursor) : null) + ) + } catch (error) { + console.error(`Error fetching release tags for wrappers federated pages: ${error}`) + return null + } +} + // Each external docs page is mapped to a local page const pageMap = [ { @@ -193,10 +261,18 @@ const getContent = async (params: Params) => { isExternal = true let remoteFile: string ;({ remoteFile, meta } = federatedPage) - const repoPath = `${org}/${repo}/${branch}/${docsDir}/${remoteFile}` - editLink = `${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}` - const response = await fetchRevalidatePerDay(`https://raw.githubusercontent.com/${repoPath}`) + const tag = await getLatestRelease() + if (!tag) { + throw new Error('No latest release found for federated wrappers pages') + } + + const repoPath = `${org}/${repo}/${tag}/${docsDir}/${remoteFile}` + editLink = `${org}/${repo}/blob/${tag}/${docsDir}/${remoteFile}` + + const response = await fetch(`https://raw.githubusercontent.com/${repoPath}`, { + next: { tags: [REVALIDATION_TAGS.WRAPPERS] }, + }) const rawContent = await response.text() const { content: contentWithoutFrontmatter } = matter(rawContent) @@ -257,4 +333,4 @@ const generateStaticParams = async () => { const generateMetadata = genGuideMeta(getContent) export default WrappersDocs -export { generateStaticParams, generateMetadata } +export { generateMetadata, generateStaticParams } diff --git a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx index fbf0d930735..69ba3cf793b 100644 --- a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx @@ -5,11 +5,11 @@ import rehypeSlug from 'rehype-slug' import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' -import { fetchRevalidatePerDay_TEMP_TESTING } from '~/features/helpers.fetch' import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' +import { REVALIDATION_TAGS } from '~/features/helpers.fetch' export const dynamicParams = false @@ -135,8 +135,9 @@ const getContent = async ({ slug }: Params) => { const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`) - const response = await fetchRevalidatePerDay_TEMP_TESTING( - `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}` + const response = await fetch( + `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`, + { next: { tags: [REVALIDATION_TAGS.GRAPHQL] } } ) const content = await response.text() diff --git a/apps/docs/app/guides/realtime/[[...slug]]/page.tsx b/apps/docs/app/guides/realtime/[[...slug]]/page.tsx index 7a19de3cd7b..79e91ca9222 100644 --- a/apps/docs/app/guides/realtime/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/realtime/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { IS_PROD } from 'common' +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' - -export const dynamicParams = false +import { getEmptyArray } from '~/features/helpers.fn' type Params = { slug?: string[] } @@ -16,10 +16,10 @@ const RealtimeGuidePage = async ({ params }: { params: Params }) => { return } -const generateStaticParams = genGuidesStaticParams('realtime') +const generateStaticParams = IS_PROD ? genGuidesStaticParams('realtime') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['realtime', ...(params.slug ?? [])]) ) export default RealtimeGuidePage -export { generateStaticParams, generateMetadata } +export { generateMetadata, generateStaticParams } diff --git a/apps/docs/app/guides/storage/[[...slug]]/page.tsx b/apps/docs/app/guides/storage/[[...slug]]/page.tsx index bf1754495ac..0f379466f54 100644 --- a/apps/docs/app/guides/storage/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/storage/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { IS_PROD } from 'common' +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' - -export const dynamicParams = false +import { getEmptyArray } from '~/features/helpers.fn' type Params = { slug?: string[] } @@ -16,10 +16,10 @@ const StorageGuidePage = async ({ params }: { params: Params }) => { return } -const generateStaticParams = genGuidesStaticParams('storage') +const generateStaticParams = IS_PROD ? genGuidesStaticParams('storage') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['storage', ...(params.slug ?? [])]) ) export default StorageGuidePage -export { generateStaticParams, generateMetadata } +export { generateMetadata, generateStaticParams } diff --git a/apps/docs/codegen.ts b/apps/docs/codegen.ts new file mode 100644 index 00000000000..6b52a4cc4c5 --- /dev/null +++ b/apps/docs/codegen.ts @@ -0,0 +1,13 @@ +import type { CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = { + overwrite: true, + schema: './__generated__/schema.graphql', + generates: { + '__generated__/graphql.ts': { + plugins: ['typescript', 'typescript-resolvers'], + }, + }, +} + +export default config diff --git a/apps/docs/components/Feedback/Feedback.tsx b/apps/docs/components/Feedback/Feedback.tsx index 16771ca6e5b..e2aa841aa43 100644 --- a/apps/docs/components/Feedback/Feedback.tsx +++ b/apps/docs/components/Feedback/Feedback.tsx @@ -18,7 +18,7 @@ import { Button, cn } from 'ui' import { IS_PLATFORM } from '~/lib/constants' import { useSendFeedbackMutation } from '~/lib/fetch/feedback' import { useSendTelemetryEvent } from '~/lib/telemetry' -import { getNotionTeam, getSanitizedTabParams } from './Feedback.utils' +import { getLinearTeam, getSanitizedTabParams } from './Feedback.utils' import { type FeedbackFields, FeedbackModal } from './FeedbackModal' const FeedbackButton = forwardRef< @@ -134,7 +134,7 @@ function Feedback({ className }: { className?: string }) { title, // @ts-expect-error -- can't click this button without having a state.response isHelpful: state.response === 'yes', - team: getNotionTeam(pathname), + team: getLinearTeam(pathname), }) setModalOpen(false) refocusButton() diff --git a/apps/docs/components/Feedback/Feedback.utils.ts b/apps/docs/components/Feedback/Feedback.utils.ts index cf9ff4c989a..2fa626ce14d 100644 --- a/apps/docs/components/Feedback/Feedback.utils.ts +++ b/apps/docs/components/Feedback/Feedback.utils.ts @@ -3,8 +3,8 @@ import { pick } from 'lodash' /** * Gets the Notion team to send feedback to based on the pathname. */ -const getNotionTeam = (pathname: string) => { - const DEFAULT_TEAM = 'team-docs' +const getLinearTeam = (pathname: string) => { + const DEFAULT_TEAM = 'Docs' // Pathname has format `/guides/(team)/**` const pathParts = pathname.split('/') @@ -13,26 +13,26 @@ const getNotionTeam = (pathname: string) => { switch (pathParts[2]) { case 'database': - return 'team-postgres' + return 'Postgres' case 'auth': - return 'team-auth' + return 'Auth' case 'storage': - return 'team-storage' + return 'Storage' case 'functions': - return 'team-functions' + return 'Functions' case 'realtime': - return 'team-realtime' + return 'Realtime' case 'ai': - return 'team-ai' + return 'AI' case 'local-development': case 'self-hosting': case 'deployment': - return 'team-dev-workflows' + return 'Dev Workflows' case 'integrations': - return 'team-api' + return 'API' case 'platform': case 'monitoring-troubleshooting': - return 'team-infra' + return 'Infra' default: return DEFAULT_TEAM } @@ -51,4 +51,4 @@ const getSanitizedTabParams = () => { return pick(Object.fromEntries(searchParams.entries()), queryGroups) } -export { getNotionTeam, getSanitizedTabParams } +export { getLinearTeam, getSanitizedTabParams } diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx index cef95a4119e..cc6c052a369 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx @@ -169,9 +169,9 @@ const Content = (props) => { ) })} - ) : ( + ) : x.url ? ( - )} + ) : null} ) })} diff --git a/apps/docs/content/guides/auth/quickstarts/nextjs.mdx b/apps/docs/content/guides/auth/quickstarts/nextjs.mdx index 90b0bec7d26..69e6a29ef44 100644 --- a/apps/docs/content/guides/auth/quickstarts/nextjs.mdx +++ b/apps/docs/content/guides/auth/quickstarts/nextjs.mdx @@ -52,7 +52,7 @@ hideToc: true - Rename `.env.local.example` to `.env.local` and populate with [your project's URL and Anon Key](https://supabase.com/dashboard/project/_/settings/api). + Rename `.env.example` to `.env.local` and populate with [your project's URL and Anon Key](https://supabase.com/dashboard/project/_/settings/api). diff --git a/apps/docs/content/guides/auth/social-login/auth-apple.mdx b/apps/docs/content/guides/auth/social-login/auth-apple.mdx index 0316b186396..aa2faa122e1 100644 --- a/apps/docs/content/guides/auth/social-login/auth-apple.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-apple.mdx @@ -14,7 +14,7 @@ To support Sign in with Apple, you need to configure the [Apple provider in the There are three general ways to use Sign in with Apple, depending on the application you're trying to build: - Sign in on the web or in web-based apps - - Using an OAuth flow initiated by Supabase Auth using the [Sign in with Apple REST API](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api). + - Using an OAuth flow initiated by Supabase Auth using the [Sign in with Apple REST API](https://developer.apple.com/documentation/signinwithapplerestapi). - Using [Sign in with Apple JS](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js) directly in the browser, usually suitable for websites. - Sign in natively inside iOS, macOS, watchOS or tvOS apps using [Apple's Authentication Services](https://developer.apple.com/documentation/authenticationservices) @@ -35,7 +35,7 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app, Sign in with Apple's OAuth flow is designed for web or browser based sign in methods. It can be used on web-based apps as well as websites, though some users can benefit by using Sign in with Apple JS directly. - Behind the scenes, Supabase Auth uses the [REST APIs](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api) provided by Apple. + Behind the scenes, Supabase Auth uses the [REST APIs](https://developer.apple.com/documentation/signinwithapplerestapi) provided by Apple. <$Partial path="create_client_snippet.mdx" /> diff --git a/apps/docs/content/guides/auth/third-party/clerk.mdx b/apps/docs/content/guides/auth/third-party/clerk.mdx index 3a422696986..852eb4dedc2 100644 --- a/apps/docs/content/guides/auth/third-party/clerk.mdx +++ b/apps/docs/content/guides/auth/third-party/clerk.mdx @@ -37,15 +37,11 @@ If you are not able to use [Clerk's Connect with Supabase page](https://dashboar -```typescript -import { createClient } from '@supabase/supabase-js' - -const supabase = createClient('https://.supabase.co', 'SUPABASE_ANON_KEY', { - accessToken: () => { - return Clerk.session?.getToken() - }, -}) -``` +<$CodeSample +path="/clerk/hooks/useSupabaseClient.ts" +lines={[[6, 14]]} +hideElidedLines={true} +/> @@ -99,34 +95,23 @@ The recommended way to design RLS policies with Clerk is to use claims present i ### Example: Check user organization role -```sql -create policy "Only organization admins can insert in table" - on table_name - for insert - to authenticated - with check ( - ((select auth.jwt()->>'org_role') = 'org:admin') - and - (organization_id = (select auth.jwt()->>'org_id')) - ); -``` +<$CodeSample +path="/clerk/supabase/migrations/20250501155648_setup_database.sql" +lines={[[10, 18]]} +hideElidedLines={true} +/> -This RLS policy checks that the newly inserted row in the table has the user's declared organization ID (from the `org_id` session token claim) in the `organization_id` column. Additionally it ensures that they're an `org:admin`. +This RLS policy checks that the newly inserted row in the table has the user's declared organization ID in the `organization_id` column. Additionally it ensures that they're an `org:admin`. This way only organization admins can add rows to the table, for organizations they're a member of. ### Example: Check user has passed second factor verification -```sql -create policy "Only users that have passed second factor verification can read from table" - on table_name - for select - as restrictive - to authenticated - using ( - ((select auth.jwt()->'fva'->>1) != '-1') - ); -``` +<$CodeSample +path="/clerk/supabase/migrations/20250501155648_setup_database.sql" +lines={[[28, 35]]} +hideElidedLines={true} +/> This example uses a restrictive RLS policy checks that the [second factor verification](https://clerk.com/docs/guides/reverification) age element in the `fva` claim is not `'-1'` indicating the user has passed through second factor verification. diff --git a/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx b/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx index fe490755a0b..17e8782a234 100644 --- a/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx +++ b/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx @@ -8,19 +8,9 @@ description: 'Block queries over a total cost limit' ## Enable the extension -`pg_plan_filter` can be enabled on a per connection basis: +The extension is already enabled by default via `shared_preload_libraries` setting. -{/* prettier-ignore */} -```sql -load 'plan_filter'; -``` - -or for all connections: - -{/* prettier-ignore */} -```sql -alter database some_db set session_preload_libraries = 'plan_filter'; -``` +You can follow the instructions below. ## API @@ -66,7 +56,6 @@ Now we can choose a `statement_cost_filter` value between the total cost for the {/* prettier-ignore */} ```sql -load 'plan_filter'; set plan_filter.statement_cost_limit = 50; -- between 2.49 and 135.0 select * from book where id = 1; diff --git a/apps/docs/content/guides/database/json.mdx b/apps/docs/content/guides/database/json.mdx index 5a067043f46..1619c015cb6 100644 --- a/apps/docs/content/guides/database/json.mdx +++ b/apps/docs/content/guides/database/json.mdx @@ -417,7 +417,7 @@ from books; ```js const { data, error } = await supabase.from('books').select(` title, - description: metadata->description, + description: metadata->>description, price: metadata->price, low_age: metadata->ages->0, high_age: metadata->ages->1 @@ -433,7 +433,7 @@ try await supabase .select( """ title, - description: metadata->description, + description: metadata->>description, price: metadata->price, low_age: metadata->ages->0, high_age: metadata->ages->1 @@ -448,7 +448,7 @@ try await supabase ```kotlin val data = supabase.from("books").select(Columns.raw(""" title, - description: metadata->description, + description: metadata->>description, price: metadata->price, low_age: metadata->ages->0, high_age: metadata->ages->1 @@ -461,7 +461,7 @@ val data = supabase.from("books").select(Columns.raw(""" ```python data = supabase.from_('books').select(""" title, - description: metadata->description, + description: metadata->>description, price: metadata->price, low_age: metadata->ages->0, high_age: metadata->ages->1 diff --git a/apps/docs/content/guides/database/webhooks.mdx b/apps/docs/content/guides/database/webhooks.mdx index 92d58ff2fb8..ab35a734d6a 100644 --- a/apps/docs/content/guides/database/webhooks.mdx +++ b/apps/docs/content/guides/database/webhooks.mdx @@ -27,7 +27,7 @@ This video demonstrates how you can create a new customer in Stripe each time a ## Creating a webhook -1. Create a new [Database Webhook](https://supabase.com/dashboard/project/_/integrations/hooks) in the Dashboard. +1. Create a new [Database Webhook](https://supabase.com/dashboard/project/_/integrations/webhooks/overview) in the Dashboard. 1. Give your Webhook a name. 1. Select the table you want to hook into. 1. Select one or more events (table inserts, updates, or deletes) you want to hook into. diff --git a/apps/docs/content/guides/deployment/database-migrations.mdx b/apps/docs/content/guides/deployment/database-migrations.mdx index bae1145219b..6495069cbf5 100644 --- a/apps/docs/content/guides/deployment/database-migrations.mdx +++ b/apps/docs/content/guides/deployment/database-migrations.mdx @@ -303,7 +303,7 @@ supabase link - + [Push](/docs/reference/cli/supabase-db-push) your migrations to the remote database. @@ -318,4 +318,22 @@ supabase db push + + + + + [Push](/docs/reference/cli/supabase-db-push) your migrations and seed the remote database. + + + + +```bash name=Terminal +supabase db push --include-seed +``` + + + + + + Visiting your live project on [Supabase](https://supabase.com/dashboard/project/_), you'll see a new `employees` table, complete with the `department` column you added in the second migration above. diff --git a/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx b/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx index cb41c4408c4..1705ea759e4 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx @@ -367,9 +367,9 @@ enum TransferError: Error { <$CodeTabs> ```swift name=ProfileView.swift -+ import PhotosUI -+ import Storage -+ import Supabase +import PhotosUI +import Storage +import Supabase import SwiftUI struct ProfileView: View { @@ -379,34 +379,34 @@ struct ProfileView: View { @State var isLoading = false -+ @State var imageSelection: PhotosPickerItem? -+ @State var avatarImage: AvatarImage? + @State var imageSelection: PhotosPickerItem? + @State var avatarImage: AvatarImage? var body: some View { NavigationStack { Form { -+ Section { -+ HStack { -+ Group { -+ if let avatarImage { -+ avatarImage.image.resizable() -+ } else { -+ Color.clear -+ } -+ } -+ .scaledToFit() -+ .frame(width: 80, height: 80) -+ -+ Spacer() -+ -+ PhotosPicker(selection: $imageSelection, matching: .images) { -+ Image(systemName: "pencil.circle.fill") -+ .symbolRenderingMode(.multicolor) -+ .font(.system(size: 30)) -+ .foregroundColor(.accentColor) -+ } -+ } -+ } + Section { + HStack { + Group { + if let avatarImage { + avatarImage.image.resizable() + } else { + Color.clear + } + } + .scaledToFit() + .frame(width: 80, height: 80) + + Spacer() + + PhotosPicker(selection: $imageSelection, matching: .images) { + Image(systemName: "pencil.circle.fill") + .symbolRenderingMode(.multicolor) + .font(.system(size: 30)) + .foregroundColor(.accentColor) + } + } + } Section { TextField("Username", text: $username) @@ -440,10 +440,10 @@ struct ProfileView: View { } } }) -+ .onChange(of: imageSelection) { _, newValue in -+ guard let newValue else { return } -+ loadTransferable(from: newValue) -+ } + .onChange(of: imageSelection) { _, newValue in + guard let newValue else { return } + loadTransferable(from: newValue) + } } .task { await getInitialProfile() @@ -467,9 +467,9 @@ struct ProfileView: View { fullName = profile.fullName ?? "" website = profile.website ?? "" -+ if let avatarURL = profile.avatarURL, !avatarURL.isEmpty { -+ try await downloadImage(path: avatarURL) -+ } + if let avatarURL = profile.avatarURL, !avatarURL.isEmpty { + try await downloadImage(path: avatarURL) + } } catch { debugPrint(error) @@ -481,7 +481,7 @@ struct ProfileView: View { isLoading = true defer { isLoading = false } do { -+ let imageURL = try await uploadImage() + let imageURL = try await uploadImage() let currentUser = try await supabase.auth.session.user @@ -489,7 +489,7 @@ struct ProfileView: View { username: username, fullName: fullName, website: website, -+ avatarURL: imageURL + avatarURL: imageURL ) try await supabase @@ -503,36 +503,36 @@ struct ProfileView: View { } } -+ private func loadTransferable(from imageSelection: PhotosPickerItem) { -+ Task { -+ do { -+ avatarImage = try await imageSelection.loadTransferable(type: AvatarImage.self) -+ } catch { -+ debugPrint(error) -+ } -+ } -+ } -+ -+ private func downloadImage(path: String) async throws { -+ let data = try await supabase.storage.from("avatars").download(path: path) -+ avatarImage = AvatarImage(data: data) -+ } -+ -+ private func uploadImage() async throws -> String? { -+ guard let data = avatarImage?.data else { return nil } -+ -+ let filePath = "\(UUID().uuidString).jpeg" -+ -+ try await supabase.storage -+ .from("avatars") -+ .upload( -+ filePath, -+ data: data, -+ options: FileOptions(contentType: "image/jpeg") -+ ) -+ -+ return filePath -+ } + private func loadTransferable(from imageSelection: PhotosPickerItem) { + Task { + do { + avatarImage = try await imageSelection.loadTransferable(type: AvatarImage.self) + } catch { + debugPrint(error) + } + } + } + + private func downloadImage(path: String) async throws { + let data = try await supabase.storage.from("avatars").download(path: path) + avatarImage = AvatarImage(data: data) + } + + private func uploadImage() async throws -> String? { + guard let data = avatarImage?.data else { return nil } + + let filePath = "\(UUID().uuidString).jpeg" + + try await supabase.storage + .from("avatars") + .upload( + filePath, + data: data, + options: FileOptions(contentType: "image/jpeg") + ) + + return filePath + } } ``` diff --git a/apps/docs/content/guides/integrations/build-a-supabase-integration/oauth-scopes.mdx b/apps/docs/content/guides/integrations/build-a-supabase-integration/oauth-scopes.mdx index 3ce86288029..ef13b111f2c 100644 --- a/apps/docs/content/guides/integrations/build-a-supabase-integration/oauth-scopes.mdx +++ b/apps/docs/content/guides/integrations/build-a-supabase-integration/oauth-scopes.mdx @@ -15,6 +15,8 @@ Scopes restrict access to the specific [Supabase Management API endpoints](/docs Scopes are set when you [create an OAuth app](/docs/guides/platform/oauth-apps/build-a-supabase-integration#create-an-oauth-app) in the Supabase Dashboard. +You can update scopes of your OAuth app at any time, but existing OAuth app users will need to re-authorize your app via the [OAuth flow](/docs/guides/integrations/build-a-supabase-integration#implementing-the-oauth-20-flow) to apply the new scopes. + ## Available scopes | Name | Type | Description | diff --git a/apps/docs/content/guides/local-development.mdx b/apps/docs/content/guides/local-development.mdx index e96f5b7a6cc..3ca06e804b8 100644 --- a/apps/docs/content/guides/local-development.mdx +++ b/apps/docs/content/guides/local-development.mdx @@ -52,7 +52,7 @@ Develop locally while running the Supabase stack on your machine. ```sh - pnpm supabase init + pnpx supabase init ``` @@ -82,7 +82,7 @@ Develop locally while running the Supabase stack on your machine. ```sh - pnpm supabase start + pnpx supabase start ``` diff --git a/apps/docs/content/guides/local-development/declarative-database-schemas.mdx b/apps/docs/content/guides/local-development/declarative-database-schemas.mdx index 26eccaca0a3..6787f77ba8a 100644 --- a/apps/docs/content/guides/local-development/declarative-database-schemas.mdx +++ b/apps/docs/content/guides/local-development/declarative-database-schemas.mdx @@ -42,6 +42,12 @@ create table "employees" ( + + +Make sure your local database is stopped before diffing your schema. + + + @@ -64,17 +70,11 @@ supabase db diff -f create_employees_table - - -Make sure your local database is stopped before diffing your schema. - - - - - The new migration file will be used when starting the database locally so you can validate your schema using the local Dashboard. + + Start the local database and then apply the migration file to see your schema changes in the local Dashboard. @@ -82,7 +82,7 @@ Make sure your local database is stopped before diffing your schema. <$CodeTabs> ```bash name=Terminal -supabase start +supabase start && supabase migration up ``` diff --git a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-sso.mdx b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-sso.mdx index 1750bc78e51..1c33b9f09f8 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-sso.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-sso.mdx @@ -5,7 +5,7 @@ title: 'Manage Monthly Active SSO Users usage' ## What you are charged for -You are charged for the number of distinct users who log in or refresh their token during the billing cycle using a SAML 2.0 compatible identity provider. Each unique user is counted only once per billing cycle, regardless of how many times they authenticate. These users are referred to as "SSO MAUs". +You are charged for the number of distinct users who log in or refresh their token during the billing cycle using a SAML 2.0 compatible identity provider (e.g. Google Workspace, Microsoft Active Directory). Each unique user is counted only once per billing cycle, regardless of how many times they authenticate. These users are referred to as "SSO MAUs". ### Example diff --git a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-third-party.mdx b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-third-party.mdx index b120282ef04..75669c9e67a 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-third-party.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users-third-party.mdx @@ -5,7 +5,7 @@ title: 'Manage Monthly Active Third-Party Users usage' ## What you are charged for -You are charged for the number of distinct users who log in or refresh their token during the billing cycle using a third-party authentication provider. Each unique user is counted only once per billing cycle, regardless of how many times they authenticate. These users are referred to as "Third-Party MAUs". +You are charged for the number of distinct users who log in or refresh their token during the billing cycle using a third-party authentication provider (Clerk, Firebase Auth, Auth0, AWS Cognito). Each unique user is counted only once per billing cycle, regardless of how many times they authenticate. These users are referred to as "Third-Party MAUs". ### Example diff --git a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users.mdx b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users.mdx index bb72b33078c..678ead17d40 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/monthly-active-users.mdx @@ -5,7 +5,7 @@ title: 'Manage Monthly Active Users usage' ## What you are charged for -You are charged for the number of distinct users who log in or refresh their token during the billing cycle. Each unique user is counted only once per billing cycle, regardless of how many times they authenticate. These users are referred to as "MAUs". +You are charged for the number of distinct users who log in or refresh their token during the billing cycle (including Social Login with e.g. Google, Facebook, GitHub). Each unique user is counted only once per billing cycle, regardless of how many times they authenticate. These users are referred to as "MAUs". ### Example diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index 37fa3f7346b..6ab96ea03b4 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -4,7 +4,13 @@ title: Upgrading Supabase ships fast and we endeavor to add all new features to existing projects wherever possible. In some cases, access to new features require upgrading or migrating your Supabase project. -You can upgrade your project using `pg_upgrade` or by pausing and restoring your project. + + +This guide refers to upgrading the Postgres version of your Supabase Project. For scaling your compute size, refer to the [Compute and Disk page](/docs/guides/platform/compute-and-disk). + + + +You can upgrade your project using in-place upgrades or by pausing and restoring your project. @@ -14,33 +20,27 @@ The Migrating and Upgrading guide has been divided into two sections. To migrate -## `pg_upgrade` - - - -This upgrade method is currently in Beta. - - +## In-place upgrades For security purposes, passwords for custom roles are not backed up and, following a restore, they would need to be reset. See [here](/docs/guides/platform/backups#daily-backups) for more details -pg_upgrade performs an in-place upgrade on your database. For projects larger than 1GB, pg_upgrade is generally faster than a pause+restore cycle, and the speed advantage grows with the size of the database. +In-place upgrades uses `pg_upgrade`. For projects larger than 1GB, this method is generally faster than a pause and restore cycle, and the speed advantage grows with the size of the database. 1. Plan for an appropriate downtime window, and ensure you have reviewed the [caveats](#caveats) section of this document before executing the upgrade. 1. Use the "Upgrade project" button on the [Infrastructure](https://supabase.com/dashboard/project/_/settings/infrastructure) section of your dashboard. -Additionally, if a pg_upgrade upgrade should fail, your original DB would be brought back up online and be able to service requests. +Additionally, if the upgrade should fail, your original database would be brought back up online and be able to service requests. -As a rough rule of thumb, pg_upgrade operates at ~100MBps (when executing an upgrade on your data). Using the size of your database, you can use this metric to derive an approximate sense of the downtime window necessary for the upgrade. During this window, you should plan for your DB and associated services to be unavailable. +As a rough rule of thumb, pg_upgrade operates at ~100MBps (when executing an upgrade on your data). Using the size of your database, you can use this metric to derive an approximate sense of the downtime window necessary for the upgrade. During this window, you should plan for your database and associated services to be unavailable. ## Pause and restore -We recommend using the pg_upgrade method, as it is faster, and more reliable. Additionally, only Free-tier projects are eligible to use the Pause + Restore method. +We recommend using the In-place upgrade method, as it is faster, and more reliable. Additionally, only Free-tier projects are eligible to use the Pause and Restore method. @@ -106,18 +106,18 @@ When upgrading, the Supabase platform will "right-size" your disk based on the c #### Objects dependent on Postgres extensions -pg_upgrade does not support upgrading of databases containing reg\* data types referencing system OIDs. +In-place upgrades do not support upgrading of databases containing reg\* data types referencing system OIDs. If you have created any objects that depend on the following extensions, you will need to recreate them after the upgrade. #### `pg_cron` records [pg_cron](https://github.com/citusdata/pg_cron#viewing-job-run-details) does not automatically clean up historical records. This can lead to extremely large `cron.job_run_details` tables if the records are not regularly pruned; you should clean unnecessary records from this table prior to an upgrade. -During the `pg_upgrade` process, the `pg_cron` extension gets dropped and recreated. Prior to this process, the `cron.job_run_details` table is duplicated to avoid losing historical logs. The instantaneous disk pressure created by duplicating an extremely large details table can cause at best unnecessary performance degradation, or at worst, upgrade process failures. +During an in-place upgrade, the `pg_cron` extension gets dropped and recreated. Prior to this process, the `cron.job_run_details` table is duplicated to avoid losing historical logs. The instantaneous disk pressure created by duplicating an extremely large details table can cause at best unnecessary performance degradation, or at worst, upgrade process failures. #### Extensions -pg_upgrade does not currently support upgrading of databases using extensions older than the following versions: +In-place upgrades do not currently support upgrading of databases using extensions older than the following versions: - TimescaleDB 2.16.1 - plv8 3.1.10 diff --git a/apps/docs/features/app.providers.tsx b/apps/docs/features/app.providers.tsx index 63b3ace9bab..f5c4b77b007 100644 --- a/apps/docs/features/app.providers.tsx +++ b/apps/docs/features/app.providers.tsx @@ -11,7 +11,7 @@ import { QueryClientProvider } from './data/queryClient.client' import { PageTelemetry } from './telemetry/telemetry.client' import { ScrollRestoration } from './ui/helpers.scroll.client' import { ThemeSandbox } from './ui/theme.client' -import { PromoToast } from 'ui-patterns' +// import { PromoToast } from 'ui-patterns' /** * Global providers that wrap the entire app @@ -28,7 +28,7 @@ function GlobalProviders({ children }: PropsWithChildren) {
- + {/* */} {children} diff --git a/apps/docs/features/directives/CodeSample.ts b/apps/docs/features/directives/CodeSample.ts index 8b92b746d09..cc425a51f31 100644 --- a/apps/docs/features/directives/CodeSample.ts +++ b/apps/docs/features/directives/CodeSample.ts @@ -10,6 +10,7 @@ * path="/path/to/file.ts" * lines={[1, 2], [5, 7]} // -1 may be used in end position as an alias for the last line, e.g., [1, -1] * meta="utils/client.ts" // Optional, for displaying a file path on the code block + * hideElidedLines={true} // Optional, for hiding elided lines in the code block * /> * ``` * @@ -24,6 +25,7 @@ * path="/path/to/file.ts" * lines={[1, 2], [5, 7]} // -1 may be used in end position as an alias for the last line, e.g., [1, -1] * meta="utils/client.ts" // Optional, for displaying a file path on the code block + * hideElidedLines={true} // Optional, for hiding elided lines in the code block * /> */ @@ -77,6 +79,7 @@ const codeSampleExternalSchema = z.object({ path: z.string().transform((v) => (v.startsWith('/') ? v : `/${v}`)), lines: linesValidator, meta: z.string().optional(), + hideElidedLines: z.coerce.boolean().default(false), }) type ICodeSampleExternal = z.infer & AdditionalMeta @@ -88,6 +91,7 @@ const codeSampleInternalSchema = z.object({ path: z.string().transform((v) => (v.startsWith('/') ? v : `/${v}`)), lines: linesValidator, meta: z.string().optional(), + hideElidedLines: z.coerce.boolean().default(false), }) type ICodeSampleInternal = z.infer & AdditionalMeta @@ -147,6 +151,9 @@ async function fetchSourceCodeContent(tree: Root, deps: Dependencies) { const path = getAttributeValue(node, 'path') const lines = getAttributeValueExpression(getAttributeValue(node, 'lines')) const meta = getAttributeValue(node, 'meta') + const hideElidedLines = getAttributeValueExpression( + getAttributeValue(node, 'hideElidedLines') + ) const result = codeSampleExternalSchema.safeParse({ external: isExternal, @@ -156,6 +163,7 @@ async function fetchSourceCodeContent(tree: Root, deps: Dependencies) { path, lines, meta, + hideElidedLines, }) if (!result.success) { @@ -186,12 +194,16 @@ async function fetchSourceCodeContent(tree: Root, deps: Dependencies) { const path = getAttributeValue(node, 'path') const lines = getAttributeValueExpression(getAttributeValue(node, 'lines')) const meta = getAttributeValue(node, 'meta') + const hideElidedLines = getAttributeValueExpression( + getAttributeValue(node, 'hideElidedLines') + ) const result = codeSampleInternalSchema.safeParse({ external: isExternal, path, lines, meta, + hideElidedLines, }) if (!result.success) { @@ -230,7 +242,7 @@ function rewriteNodes(contentMap: Map { - if (index !== 0 || start !== 1) { + if (!hideElidedLines && (index !== 0 || start !== 1)) { acc.push(_createElidedLine(lang, contentLines, start, end)) } // Start and end are 1-indexed and inclusive acc.push(...contentLines.slice(start - 1, end === -1 ? contentLines.length : end)) - if (index === arr.length - 1 && end !== -1 && end !== contentLines.length) { + if (!hideElidedLines && index === arr.length - 1 && end !== -1 && end !== contentLines.length) { acc.push(_createElidedLine(lang, contentLines, start, end)) } diff --git a/apps/docs/features/docs/Reference.mdx.tsx b/apps/docs/features/docs/Reference.mdx.tsx index 1024e5771ad..d6abe802323 100644 --- a/apps/docs/features/docs/Reference.mdx.tsx +++ b/apps/docs/features/docs/Reference.mdx.tsx @@ -1,13 +1,14 @@ import matter from 'gray-matter' import { readFile } from 'node:fs/promises' import { join } from 'node:path' - +import { type ComponentProps } from 'react' import CliGlobalFlagsHandler from '~/components/reference/enrichments/cli/CliGlobalFlagsHandler' import { MDXRemoteBase } from '~/features/docs/MdxBase' import { components } from '~/features/docs/MdxBase.shared' import { RefSubLayout } from '~/features/docs/Reference.ui' import { cache_fullProcess_withDevCacheBust } from '~/features/helpers.fs' import { REF_DOCS_DIRECTORY } from '~/lib/docs' +import { CodeBlock } from '../ui/CodeBlock/CodeBlock' async function getRefMarkdownInternal(relPath: string) { const fullPath = join(REF_DOCS_DIRECTORY, relPath + '.mdx') @@ -42,7 +43,14 @@ interface MDXRemoteRefsProps { } function MDXRemoteRefs({ source }: MDXRemoteRefsProps) { - const refComponents = { ...components, RefSubLayout, CliGlobalFlagsHandler } + const refComponents = { + ...components, + // Override the CodeBlock used for normal guides to skip type generation + // because it is too resource-intensive + pre: (props: ComponentProps) => , + RefSubLayout, + CliGlobalFlagsHandler, + } return } diff --git a/apps/docs/features/helpers.consent.tsx b/apps/docs/features/helpers.consent.tsx deleted file mode 100644 index fb4025cbd4d..00000000000 --- a/apps/docs/features/helpers.consent.tsx +++ /dev/null @@ -1,15 +0,0 @@ -'use client' - -import { useEffect } from 'react' -import { useConsent } from 'ui-patterns/ConsentToast' - -/** - * Helper client component to trigger the consent toast. It has to be a client component. - */ -export const TriggerConsentToast = () => { - const { triggerConsentToast } = useConsent() - useEffect(() => { - setTimeout(() => triggerConsentToast(), 1000) - }, [triggerConsentToast]) - return null -} diff --git a/apps/docs/features/helpers.fetch.ts b/apps/docs/features/helpers.fetch.ts index 494e0444fe4..1275a4817c1 100644 --- a/apps/docs/features/helpers.fetch.ts +++ b/apps/docs/features/helpers.fetch.ts @@ -8,6 +8,17 @@ import { ONE_DAY_IN_SECONDS } from './helpers.time' +export const REVALIDATION_TAGS = { + GRAPHQL: 'graphql', + WRAPPERS: 'wrappers', +} as const +// Casting to avoid problems with using this as a Zod enum, TypeScript does +// not recognize the casted type as a supertype of the original type +export const VALID_REVALIDATION_TAGS = Object.values(REVALIDATION_TAGS) as unknown as readonly [ + string, + ...string[], +] + function fetchWithNextOptions({ next, cache, diff --git a/apps/docs/features/helpers.fn.ts b/apps/docs/features/helpers.fn.ts index 8d5113a7030..8f50cc1c243 100644 --- a/apps/docs/features/helpers.fn.ts +++ b/apps/docs/features/helpers.fn.ts @@ -1,5 +1,10 @@ type RecObj = T[K] extends Array ? T : never +const EMPTY_ARRAY = new Array(0) +export function getEmptyArray() { + return EMPTY_ARRAY +} + export function deepFilterRec( arr: Array>, recKey: K, @@ -24,6 +29,7 @@ export function deepFilterRec( ) } -export function pluckPromise(promise: Promise, key: K) { - return promise.then((data) => data[key]) +export async function pluckPromise(promise: Promise, key: K) { + const data = await promise + return data[key] } diff --git a/apps/docs/features/helpers.misc.ts b/apps/docs/features/helpers.misc.ts new file mode 100644 index 00000000000..127958425cd --- /dev/null +++ b/apps/docs/features/helpers.misc.ts @@ -0,0 +1,17 @@ +/** + * Creates a short random identifier + */ +export function nanoId( + /** + * The length of the identifier to generate + */ + length?: number +) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + const l = length || 12 + let id = '' + for (let i = 0; i < l; i++) { + id += chars.charAt(Math.floor(Math.random() * chars.length)) + } + return id +} diff --git a/apps/docs/features/telemetry/telemetry.client.tsx b/apps/docs/features/telemetry/telemetry.client.tsx index ee1853b8d74..54b492e7989 100644 --- a/apps/docs/features/telemetry/telemetry.client.tsx +++ b/apps/docs/features/telemetry/telemetry.client.tsx @@ -1,11 +1,11 @@ 'use client' import { IS_PLATFORM, PageTelemetry as PageTelemetryImpl } from 'common' -import { useConsent } from 'ui-patterns/ConsentToast' +import { useConsentToast } from 'ui-patterns/consent' import { API_URL } from '~/lib/constants' const PageTelemetry = () => { - const { hasAcceptedConsent } = useConsent() + const { hasAcceptedConsent } = useConsentToast() return ( ) { let code = (contents || extractCode(children)).trim() const lang = tryToBundledLanguage(langSetting) || extractLang(children) let twoslashed = null as null | Map>> - if (TWOSLASHABLE_LANGS.includes(lang)) { + if (!skipTypeGeneration && TWOSLASHABLE_LANGS.includes(lang)) { try { const { code: editedCode, nodes } = twoslasher(code) const hoverNodes: Array = nodes.filter((node) => node.type === 'hover') diff --git a/apps/docs/lib/octokit.ts b/apps/docs/lib/octokit.ts index bfe5a0f1b88..edad471ae79 100644 --- a/apps/docs/lib/octokit.ts +++ b/apps/docs/lib/octokit.ts @@ -8,7 +8,7 @@ import { fetchRevalidatePerDay } from '~/features/helpers.fetch' let octokitInstance: Octokit -function octokit() { +export function octokit() { if (!octokitInstance) { // https://github.com/gr2m/universal-github-app-jwt?tab=readme-ov-file#converting-pkcs1-to-pkcs8 const privateKeyPkcs8 = crypto diff --git a/apps/docs/package.json b/apps/docs/package.json index 4208f151170..17303a070e9 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -4,11 +4,11 @@ "private": true, "scripts": { "preinstall": "npx only-allow pnpm", - "predev": "pnpm run codegen:references && pnpm run codegen:examples", + "predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples", "dev": "concurrently --kill-others \"next dev --port 3001\" \"pnpm run dev:watch:troubleshooting\"", "dev:watch:troubleshooting": "node ./scripts/troubleshooting/watch.mjs", "dev:secrets:pull": "AWS_PROFILE=supabase-dev node ../../scripts/getSecrets.js -n local/docs", - "prebuild": "pnpm run codegen:references && pnpm run codegen:examples", + "prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples", "build": "next build", "build:analyze": "ANALYZE=true next build", "build:sitemap": "tsx ./internals/generate-sitemap.ts", @@ -17,6 +17,7 @@ "start": "next start", "lint": "next lint", "lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml", + "pretypecheck": "pnpm run codegen:graphql", "typecheck": "tsc --noEmit", "pretest": "pnpm run codegen:examples", "test": "vitest --exclude \"**/*.smoke.test.ts\"", @@ -27,9 +28,10 @@ "last-changed": "tsx scripts/last-changed.ts", "last-changed:reset": "pnpm run last-changed -- --reset", "codegen:examples": "cp -r ../../examples ./examples", - "codegen:references": "tsx features/docs/Reference.generated.script.ts", "codemod:frontmatter": "node ./scripts/codemod/mdx-meta.mjs && prettier --write \"content/**/*.mdx\"", - "clean": "rimraf node_modules features/docs/generated examples" + "codegen:graphql": "tsx ./scripts/graphqlSchema.ts && graphql-codegen --config codegen.ts", + "codegen:references": "tsx features/docs/Reference.generated.script.ts", + "clean": "rimraf node_modules features/docs/generated examples __generated__" }, "dependencies": { "@har-sdk/openapi-sampler": "^2.2.0", @@ -55,6 +57,8 @@ "eslint-config-supabase": "workspace:*", "framer-motion": "^11.0.3", "github-slugger": "^2.0.0", + "graphql": "^16.10.0", + "graphql-validation-complexity": "^0.4.2", "gray-matter": "^4.0.3", "hast-util-has-property": "^2.0.1", "hast-util-to-html": "^9.0.0", @@ -105,15 +109,20 @@ "unist-util-filter": "^4.0.1", "unist-util-visit": "^4.1.2", "uuid": "^9.0.1", - "valtio": "^1.12.0", + "valtio": "catalog:", "yaml": "^2.4.5", "zod": "^3.22.4" }, "devDependencies": { "@aws-sdk/client-secrets-manager": "^3.410.0", + "@graphiql/toolkit": "^0.9.1", + "@graphql-codegen/cli": "5.0.5", + "@graphql-codegen/typescript": "4.1.6", + "@graphql-codegen/typescript-resolvers": "4.5.0", "@supabase/supa-mdx-lint": "0.2.6-alpha", "@types/common-tags": "^1.8.4", "@types/estree": "1.0.5", + "@types/graphql-validation-complexity": "^0.4.4", "@types/hast": "^2.3.4", "@types/jsrsasign": "^10.5.12", "@types/mdast": "^3.0.0", @@ -130,6 +139,7 @@ "dotenv": "^16.0.3", "ejs": "^3.1.10", "globby": "^13.2.2", + "graphiql": "^3.8.3", "openapi-types": "^12.1.3", "shiki": "^3.2.1", "simple-git": "^3.24.0", diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index ef470c4a57e..d69b658b8c8 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -32,6 +32,7 @@ David A. Ventimiglia Deji I Div Arora Divit D +Eduardo Gurgel Egor Romanov Eleftheria Trivyzaki Etienne Stalmans diff --git a/apps/docs/resources/globalSearch/globalSearchInterface.ts b/apps/docs/resources/globalSearch/globalSearchInterface.ts new file mode 100644 index 00000000000..ecf1bab91a6 --- /dev/null +++ b/apps/docs/resources/globalSearch/globalSearchInterface.ts @@ -0,0 +1,5 @@ +export interface SearchResultInterface { + title?: string + href?: string + content?: string +} diff --git a/apps/docs/resources/globalSearch/globalSearchSchema.ts b/apps/docs/resources/globalSearch/globalSearchSchema.ts new file mode 100644 index 00000000000..e62dcd06b8d --- /dev/null +++ b/apps/docs/resources/globalSearch/globalSearchSchema.ts @@ -0,0 +1,22 @@ +import { GraphQLInterfaceType, GraphQLString } from 'graphql' + +export const GRAPHQL_FIELD_SEARCH_GLOBAL = 'searchDocs' + +export const GraphQLInterfaceTypeSearchResult = new GraphQLInterfaceType({ + name: 'SearchResult', + description: 'Document that matches a search query', + fields: { + title: { + type: GraphQLString, + description: 'The title of the matching result', + }, + href: { + type: GraphQLString, + description: 'The URL of the matching result', + }, + content: { + type: GraphQLString, + description: 'The full content of the matching result', + }, + }, +}) diff --git a/apps/docs/resources/guide/guideModel.ts b/apps/docs/resources/guide/guideModel.ts new file mode 100644 index 00000000000..a1ba712600f --- /dev/null +++ b/apps/docs/resources/guide/guideModel.ts @@ -0,0 +1,37 @@ +import { SearchResultInterface } from '../globalSearch/globalSearchInterface' + +export class GuideModel implements SearchResultInterface { + public title?: string + public href?: string + public content?: string + public subsections: Array + + constructor({ + title, + href, + content, + subsections, + }: { + title?: string + href?: string + content?: string + subsections?: Array<{ title?: string; href?: string; content?: string }> + }) { + this.title = title + this.href = href + this.content = content + this.subsections = subsections?.map((subsection) => new SubsectionModel(subsection)) ?? [] + } +} + +export class SubsectionModel { + public title?: string + public href?: string + public content?: string + + constructor({ title, href, content }: { title?: string; href?: string; content?: string }) { + this.title = title + this.href = href + this.content = content + } +} diff --git a/apps/docs/resources/guide/guideSchema.ts b/apps/docs/resources/guide/guideSchema.ts new file mode 100644 index 00000000000..546ac1a3fd3 --- /dev/null +++ b/apps/docs/resources/guide/guideSchema.ts @@ -0,0 +1,56 @@ +import { GraphQLObjectType, GraphQLString } from 'graphql' +import { GraphQLInterfaceTypeSearchResult } from '../globalSearch/globalSearchSchema' +import { createCollectionType, GraphQLCollectionBuilder } from '../utils/connections' +import { GuideModel, SubsectionModel } from './guideModel' + +export const GraphQLObjectTypeSubsection = new GraphQLObjectType({ + name: 'Subsection', + isTypeOf: (value: unknown) => value instanceof SubsectionModel, + description: 'A content chunk taken from a larger document in the Supabase docs', + fields: { + title: { + type: GraphQLString, + description: 'The title of the subsection', + }, + href: { + type: GraphQLString, + description: 'The URL of the subsection', + }, + content: { + type: GraphQLString, + description: 'The content of the subsection', + }, + }, +}) + +export const GraphQLObjectTypeGuide = new GraphQLObjectType({ + name: 'Guide', + interfaces: [GraphQLInterfaceTypeSearchResult], + isTypeOf: (value: unknown) => value instanceof GuideModel, + description: + 'A document containing content from the Supabase docs. This is a guide, which might describe a concept, or explain the steps for using or implementing a feature.', + fields: { + title: { + type: GraphQLString, + description: 'The title of the document', + }, + href: { + type: GraphQLString, + description: 'The URL of the document', + }, + content: { + type: GraphQLString, + description: + 'The full content of the document, including all subsections (both those matching and not matching any query string) and possibly more content', + }, + subsections: { + type: createCollectionType(GraphQLObjectTypeSubsection, { + skipPageInfo: true, + description: 'A collection of content chunks from a larger document in the Supabase docs.', + }), + description: + 'The subsections of the document. If the document is returned from a search match, only matching content chunks are returned. For the full content of the original document, use the content field in the parent Guide.', + resolve: (node: GuideModel) => GraphQLCollectionBuilder.create({ items: node.subsections }), + }, + }, +}) diff --git a/apps/docs/resources/rootSchema.ts b/apps/docs/resources/rootSchema.ts new file mode 100644 index 00000000000..ac79f0457ff --- /dev/null +++ b/apps/docs/resources/rootSchema.ts @@ -0,0 +1,34 @@ +import { + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + printSchema, +} from 'graphql' +import { RootQueryTypeResolvers } from '~/__generated__/graphql' + +const GRAPHQL_FIELD_INTROSPECT = 'schema' as const + +type IntrospectResolver = RootQueryTypeResolvers[typeof GRAPHQL_FIELD_INTROSPECT] + +const resolveIntrospect: IntrospectResolver = async () => { + const schema = printSchema(rootGraphQLSchema) + return schema +} + +const introspectRoot = { + [GRAPHQL_FIELD_INTROSPECT]: { + description: 'Get the GraphQL schema for this endpoint', + type: new GraphQLNonNull(GraphQLString), + resolve: resolveIntrospect, + }, +} + +export const rootGraphQLSchema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'RootQueryType', + fields: { + ...introspectRoot, + }, + }), +}) diff --git a/apps/docs/resources/utils/connections.ts b/apps/docs/resources/utils/connections.ts new file mode 100644 index 00000000000..19ca714cfc3 --- /dev/null +++ b/apps/docs/resources/utils/connections.ts @@ -0,0 +1,357 @@ +import { + GraphQLBoolean, + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + type GraphQLOutputType, + GraphQLString, +} from 'graphql' +import { nanoId } from '~/features/helpers.misc' + +/** + * Extracts the name from a a GraphQLOutputType. + */ +function extractNodeTypeName( + /** + * The GraphQL Output Type to extract the name from. + */ + nodeType: GraphQLOutputType +): string | undefined { + if ('name' in nodeType) { + return nodeType.name + } else { + return `AnonymousNode(${nanoId()})` + } +} + +/** + * Creates an Edge type for a specific node type. An Edge type wraps the node + * alongside an optional associated cursor, for example: + * + * ``` + * { + * edges: [ + * { + * node: { + * id: '123', + * name: 'John Doe' + * }, + * cursor: 'YXJyYXljb25uZWN0aW9uOjE=' + * } + * ] + * } + * ``` + */ +function createEdgeType( + nodeType: GraphQLOutputType, + { + name, + skipCursor = false, + }: { + /** + * The name of the created Edge node. If not provided, defaults to + * NameOfInnerNodeEdge. + */ + name?: string + /** + * Whether to skip the cursor field. + */ + skipCursor?: boolean + } = {} +): GraphQLObjectType { + const edgeName = name || `${extractNodeTypeName(nodeType)}Edge` + + return new GraphQLObjectType({ + name: edgeName, + description: `An edge in a collection of ${extractNodeTypeName(nodeType)}s`, + fields: { + node: { + type: new GraphQLNonNull(nodeType), + description: `The ${extractNodeTypeName(nodeType)} at the end of the edge`, + }, + ...(!skipCursor && { + cursor: { + type: new GraphQLNonNull(GraphQLString), + description: 'A cursor for use in pagination', + }, + }), + }, + }) +} + +/** + * Standard GraphQL type for pagination information on connections + */ +const PageInfoType = new GraphQLObjectType({ + name: 'PageInfo', + description: 'Pagination information for a collection', + fields: { + hasNextPage: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'Whether there are more items after the current page', + }, + hasPreviousPage: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'Whether there are more items before the current page', + }, + startCursor: { + type: GraphQLString, + description: 'Cursor pointing to the start of the current page', + }, + endCursor: { + type: GraphQLString, + description: 'Cursor pointing to the end of the current page', + }, + }, +}) + +/** + * Creates a Collection type for a specific node type. A collection type + * represents a list of nodes with pagination information, and has the shape: + * + * ``` + * { + * edges: [ + * { + * node: { ... }, + * cursor: '...' + * }, + * ... + * ], + * nodes: [ ... ], + * pageInfo: { + * hasNextPage: true, + * hasPreviousPage: false, + * startCursor: '...', + * endCursor: '...' + * } + * } + * ``` + */ +export function createCollectionType( + nodeType: GraphQLOutputType, + { + name, + description, + skipPageInfo = false, + }: { + /** + * The name of the generated collection. + * + * If omitted, defaults to NameOfInnerNodeCollection. + */ + name?: string + /** + * A description of the collection that will be outputted in the generated + * schema as documentation. + */ + description?: string + /** + * Whether to skip the pageInfo field. + */ + skipPageInfo?: boolean + } = {} +): GraphQLObjectType { + const collectionName = name || `${extractNodeTypeName(nodeType)}Collection` + const edgeType = createEdgeType(nodeType, { skipCursor: skipPageInfo }) + + return new GraphQLObjectType({ + name: collectionName, + description: description || `A collection of ${extractNodeTypeName(nodeType)}s`, + fields: { + edges: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(edgeType))), + description: 'A list of edges containing nodes in this collection', + }, + nodes: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(nodeType))), + description: 'The nodes in this collection, directly accessible', + }, + ...(!skipPageInfo && { + pageInfo: { + type: new GraphQLNonNull(PageInfoType), + description: 'Pagination information', + }, + }), + totalCount: { + type: new GraphQLNonNull(GraphQLInt), + description: 'The total count of items available in this collection', + }, + }, + }) +} + +/** + * Interface for standard pagination arguments for a GraphQL connection + */ +interface IPaginationArgs { + first?: number | null + after?: string | null + last?: number | null + before?: string | null +} + +/** + * Standard pagination arguments for a GraphQL connection + */ +export const paginationArgs = { + first: { + type: GraphQLInt, + description: 'Returns the first n elements from the list', + }, + after: { + type: GraphQLString, + description: 'Returns elements that come after the specified cursor', + }, + last: { + type: GraphQLInt, + description: 'Returns the last n elements from the list', + }, + before: { + type: GraphQLString, + description: 'Returns elements that come before the specified cursor', + }, +} + +/** + * Interface for a fetch definition used to fetch a collection of items for a + * GraphQL query. Takes standard pagination args and returns standard page + * information. + */ +interface CollectionFetch { + fetch: ( + args?: IPaginationArgs & { + additionalArgs?: FetchArgs + } + ) => Promise<{ + items: Array + totalCount: number + hasNextPage?: boolean + hasPreviousPage?: boolean + }> + args?: IPaginationArgs & { + additionalArgs?: FetchArgs + } + getCursor?: (item: ItemType, idx?: number) => string + items?: never +} + +/** + * Interface for parameters used to build a collection of items from an array + * in memory + */ +interface CollectionInMemory { + items: Array + args?: IPaginationArgs + fetch?: never + getCursor?: never +} + +/** + * Union type for parameters to build a collection. Can be a remote collection + * that needs to be fetched or a local one in memory. + */ +type CollectionBuildArgs = + | CollectionFetch + | CollectionInMemory + +export class GraphQLCollectionBuilder { + static async create( + options: CollectionBuildArgs + ) { + const { fetch, args = {}, getCursor, items } = options + + if (items) { + return GraphQLCollectionBuilder.paginateArray({ items, args }) + } + + const result = await fetch(args) + const { items: fetchedItems, totalCount, hasNextPage = false, hasPreviousPage = false } = result + const edges = fetchedItems.map((item) => { + return { node: item, cursor: getCursor(item) } + }) + + return { + edges, + nodes: fetchedItems, + totalCount, + pageInfo: { + hasNextPage, + hasPreviousPage, + startCursor: edges.length > 0 ? edges[0].cursor : null, + endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null, + }, + } + } + + private static paginateArray({ items, args }: CollectionInMemory) { + const getCursor = (_item: T, idx: number) => String(idx) + const allEdges = items.map((item, idx) => { + return { node: item, cursor: getCursor(item, idx) } + }) + + const { edges, hasPreviousPage, hasNextPage } = getRequestedSlice(allEdges, args) + + return { + edges, + nodes: edges.map((edge) => edge.node), + totalCount: allEdges.length, + pageInfo: { + hasNextPage, + hasPreviousPage, + startCursor: edges.length > 0 ? edges[0].cursor : null, + endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null, + }, + } + } +} + +interface TruncatedPageInfo { + edges: Array + hasNextPage: boolean + hasPreviousPage: boolean +} + +function getRequestedSlice( + allEdges: Array, + pageArgs: IPaginationArgs +): TruncatedPageInfo { + let hasPreviousPage = false + let hasNextPage = false + + let beforeIndex = allEdges.length + let afterIndex = -1 + + const requestedBefore = toNumber(pageArgs.before) + if (requestedBefore && requestedBefore >= 0 && requestedBefore < beforeIndex) { + beforeIndex = requestedBefore + hasNextPage = true + } + const requestedAfter = toNumber(pageArgs.after) + if (requestedAfter && requestedAfter >= 0) { + afterIndex = requestedAfter + hasPreviousPage = true + } + + let edges = allEdges.slice(afterIndex + 1, beforeIndex) + + if (pageArgs.first >= 0 && edges.length > pageArgs.first) { + edges = edges.slice(0, pageArgs.first) + hasNextPage = true + } else if (pageArgs.last >= 0 && edges.length > pageArgs.last) { + edges = edges.slice(edges.length - pageArgs.last) + hasPreviousPage = true + } + + return { + edges, + hasNextPage, + hasPreviousPage, + } +} + +function toNumber(value: string): number | undefined { + const num = Number(value) + return Number.isNaN(num) ? undefined : num +} diff --git a/apps/docs/scripts/graphqlSchema.ts b/apps/docs/scripts/graphqlSchema.ts new file mode 100644 index 00000000000..17b03215ed7 --- /dev/null +++ b/apps/docs/scripts/graphqlSchema.ts @@ -0,0 +1,27 @@ +import { printSchema } from 'graphql' +import fs from 'node:fs' +import path from 'node:path' +import { rootGraphQLSchema } from '../resources/rootSchema' + +async function generateGraphQLSchema() { + try { + const schemaString = printSchema(rootGraphQLSchema) + + const outputDir = path.resolve(__dirname, '../__generated__') + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }) + } + + const outputPath = path.resolve(outputDir, 'schema.graphql') + fs.writeFileSync(outputPath, schemaString) + + console.log(`✅ Successfully generated GraphQL schema at ${outputPath}`) + } catch (error) { + console.error('🚨 Error generating GraphQL schema:', error) + process.exit(1) + } +} + +if (require.main === module) { + generateGraphQLSchema() +} diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index e8781085ddb..77a8a503195 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 2.22.4 + version: 2.22.6 title: Supabase CLI language: sh source: https://github.com/supabase/cli @@ -904,6 +904,17 @@ commands: - id: supabase-secrets title: supabase secrets summary: Manage Supabase secrets + description: | + Provides tools for managing environment variables and secrets for your Supabase project. + + This command group allows you to set, unset, and list secrets that are securely stored and + made available to Edge Functions as environment variables. + + Secrets management through the CLI is useful for: + - Setting environment-specific configuration + - Managing sensitive credentials securely + + Secrets can be set individually or loaded from .env files for convenience. tags: - management-api links: [] @@ -958,6 +969,15 @@ commands: - id: supabase-projects title: supabase projects summary: Manage Supabase projects + description: | + Provides tools for creating and managing your Supabase projects. + + This command group allows you to list all projects in your organizations, create new projects, + delete existing projects, and retrieve API keys. These operations help you manage your Supabase + infrastructure programmatically without using the dashboard. + + Project management via CLI is especially useful for automation scripts and when you need to + provision environments in a repeatable way. tags: - management-api links: [] @@ -2492,6 +2512,17 @@ commands: - id: supabase-gen-types title: supabase gen types summary: Generate types from Postgres schema + description: | + Automatically generates type definitions based on your Postgres database schema. + + This command connects to your database (local or remote) and generates typed definitions + that match your database tables, views, and stored procedures. By default, it generates TypeScript + definitions, but also supports Go and Swift. + + Generated types give you type safety and autocompletion when working with your database in code, + helping prevent runtime errors and improving developer experience. + + The types respect relationships, constraints, and custom types defined in your database schema. tags: [] links: [] usage: supabase gen types [flags] @@ -2571,6 +2602,14 @@ commands: - id: supabase-functions title: supabase functions summary: Manage Supabase Edge functions + description: | + Supabase Edge Functions are server-less functions that run close to your users. + + Edge Functions allow you to execute custom server-side code without deploying or scaling a traditional server. + They're ideal for handling webhooks, custom API endpoints, data validation, and serving personalized content. + + Edge Functions are written in TypeScript and run on Deno compatible edge runtime, which is a secure runtime + with no package management needed, fast cold starts, and built-in security. tags: - management-api links: [] @@ -2656,6 +2695,15 @@ commands: - id: supabase-functions-new title: supabase functions new summary: Create a new Function locally + description: | + Creates a new Edge Function with boilerplate code in the `supabase/functions` directory. + + This command generates a starter TypeScript file with the necessary Deno imports and a + basic function structure. The function is created as a new directory with the name you specify, + containing an `index.ts` file with the function code. + + After creating the function, you can edit it locally and then use `supabase functions serve` + to test it before deploying with `supabase functions deploy`. tags: [] links: [] usage: supabase functions new @@ -3384,6 +3432,10 @@ commands: - id: supabase-config-push title: supabase config push summary: Pushes local config.toml to the linked project + description: | + Updates the configurations of a linked Supabase project with the local `supabase/config.toml` file. + + This command allows you to manage project configuration as code by defining settings locally and then pushing them to your remote project. tags: [] links: [] usage: supabase config push diff --git a/apps/studio/components/grid/SupabaseGrid.tsx b/apps/studio/components/grid/SupabaseGrid.tsx index 942a097350c..d6dfe395600 100644 --- a/apps/studio/components/grid/SupabaseGrid.tsx +++ b/apps/studio/components/grid/SupabaseGrid.tsx @@ -62,10 +62,13 @@ export const SupabaseGrid = ({ { keepPreviousData: true, retryDelay: (retryAttempt, error: any) => { - if (error && error.message?.includes('does not exist')) { - onApplySorts([]) - } - if (retryAttempt > 3) { + const doesNotExistError = error && error.message?.includes('does not exist') + const tooManyRequestsError = error.message?.includes('Too Many Requests') + const vaultError = error.message?.includes('query vault failed') + + if (doesNotExistError) onApplySorts([]) + + if (retryAttempt > 3 || doesNotExistError || tooManyRequestsError || vaultError) { return Infinity } return 5000 @@ -82,7 +85,7 @@ export const SupabaseGrid = ({ return (
-
+
{children || ( <> diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index 50035cadc65..7fd22432211 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -7,7 +7,8 @@ import { ReactNode, useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' -import type { Filter, Sort } from 'components/grid/types' +import { useTableFilter } from 'components/grid/hooks/useTableFilter' +import { useTableSort } from 'components/grid/hooks/useTableSort' import GridHeaderActions from 'components/interfaces/TableGridEditor/GridHeaderActions' import { formatTableRowsToSQL } from 'components/interfaces/TableGridEditor/TableEntity.utils' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -34,8 +35,8 @@ import { Separator, SonnerProgress, } from 'ui' -import FilterPopover from './filter/FilterPopover' -import { SortPopover } from './sort' +import { FilterPopover } from './filter/FilterPopover' +import { SortPopover } from './sort/SortPopover' // [Joshen] CSV exports require this guard as a fail-safe if the table is // just too large for a browser to keep all the rows in memory before // exporting. Either that or export as multiple CSV sheets with max n rows each @@ -52,30 +53,24 @@ export const MAX_EXPORT_ROW_COUNT_MESSAGE = ( ) export type HeaderProps = { - sorts: Sort[] - filters: Filter[] customHeader: ReactNode } -const Header = ({ sorts: sortsProp, filters: filtersProp, customHeader }: HeaderProps) => { +const Header = ({ customHeader }: HeaderProps) => { const snap = useTableEditorTableStateSnapshot() - const filters = filtersProp - const sorts = sortsProp + // const filters = filtersProp + // const sorts = sortsProp return (
{customHeader ? ( - <>{customHeader} + customHeader + ) : snap.selectedRows.size > 0 ? ( + ) : ( - <> - {snap.selectedRows.size > 0 ? ( - - ) : ( - - )} - + )}
@@ -226,11 +221,7 @@ const DefaultHeader = () => { ) } -type RowHeaderProps = { - sorts: Sort[] - filters: Filter[] -} -const RowHeader = ({ sorts, filters }: RowHeaderProps) => { +const RowHeader = () => { const { project } = useProjectContext() const tableEditorSnap = useTableEditorStateSnapshot() const snap = useTableEditorTableStateSnapshot() @@ -238,6 +229,9 @@ const RowHeader = ({ sorts, filters }: RowHeaderProps) => { const roleImpersonationState = useRoleImpersonationStateSnapshot() const isImpersonatingRole = roleImpersonationState.role !== undefined + const { filters } = useTableFilter() + const { sorts } = useTableSort() + const [isExporting, setIsExporting] = useState(false) const { data } = useTableRowsQuery({ diff --git a/apps/studio/components/grid/components/header/filter/FilterPopover.tsx b/apps/studio/components/grid/components/header/filter/FilterPopover.tsx index 26761361f7a..cab1b6e51b1 100644 --- a/apps/studio/components/grid/components/header/filter/FilterPopover.tsx +++ b/apps/studio/components/grid/components/header/filter/FilterPopover.tsx @@ -2,13 +2,13 @@ import { useMemo } from 'react' import { useTableFilter } from 'components/grid/hooks/useTableFilter' import { formatFilterURLParams } from 'components/grid/SupabaseGrid.utils' -import FilterPopoverPrimitive from './FilterPopoverPrimitive' +import { FilterPopoverPrimitive } from './FilterPopoverPrimitive' export interface FilterPopoverProps { portal?: boolean } -const FilterPopover = ({ portal = true }: FilterPopoverProps) => { +export const FilterPopover = ({ portal = true }: FilterPopoverProps) => { const { urlFilters, onApplyFilters } = useTableFilter() // Convert string[] to Filter[] @@ -20,5 +20,3 @@ const FilterPopover = ({ portal = true }: FilterPopoverProps) => { ) } - -export default FilterPopover diff --git a/apps/studio/components/grid/components/header/filter/FilterPopoverPrimitive.tsx b/apps/studio/components/grid/components/header/filter/FilterPopoverPrimitive.tsx index 3da4bcd225f..8891ff8dcef 100644 --- a/apps/studio/components/grid/components/header/filter/FilterPopoverPrimitive.tsx +++ b/apps/studio/components/grid/components/header/filter/FilterPopoverPrimitive.tsx @@ -2,8 +2,8 @@ import { isEqual } from 'lodash' import { Filter as FilterIcon, Plus } from 'lucide-react' import { KeyboardEvent, useCallback, useMemo, useState } from 'react' -import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import type { Filter } from 'components/grid/types' +import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Button, PopoverContent_Shadcn_, @@ -20,7 +20,7 @@ export interface FilterPopoverPrimitiveProps { portal?: boolean } -const FilterPopoverPrimitive = ({ +export const FilterPopoverPrimitive = ({ buttonText, filters, onApplyFilters, @@ -134,5 +134,3 @@ const FilterPopoverPrimitive = ({ ) } - -export default FilterPopoverPrimitive diff --git a/apps/studio/components/grid/components/header/sort/SortPopover.tsx b/apps/studio/components/grid/components/header/sort/SortPopover.tsx index 6e94d073bd6..f7cc7e20400 100644 --- a/apps/studio/components/grid/components/header/sort/SortPopover.tsx +++ b/apps/studio/components/grid/components/header/sort/SortPopover.tsx @@ -3,13 +3,13 @@ import { useMemo } from 'react' import { formatSortURLParams } from 'components/grid/SupabaseGrid.utils' import { useTableSort } from 'components/grid/hooks/useTableSort' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' -import SortPopoverPrimitive from './SortPopoverPrimitive' +import { SortPopoverPrimitive } from './SortPopoverPrimitive' export interface SortPopoverProps { portal?: boolean } -const SortPopover = ({ portal = true }: SortPopoverProps) => { +export const SortPopover = ({ portal = true }: SortPopoverProps) => { const { urlSorts, onApplySorts } = useTableSort() const tableState = useTableEditorTableStateSnapshot() const tableName = tableState?.table?.name || '' @@ -21,5 +21,3 @@ const SortPopover = ({ portal = true }: SortPopoverProps) => { return } - -export default SortPopover diff --git a/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx b/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx index dd720d9f356..7cba3696a96 100644 --- a/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx +++ b/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx @@ -30,7 +30,7 @@ export interface SortPopoverPrimitiveProps { * To avoid issues with drag-and-drop reconciliation, we use a special sync mechanism * that properly detects external vs. internal updates. */ -const SortPopoverPrimitive = ({ +export const SortPopoverPrimitive = ({ buttonText, sorts, onApplySorts, @@ -234,5 +234,3 @@ const SortPopoverPrimitive = ({ ) } - -export default SortPopoverPrimitive diff --git a/apps/studio/components/grid/components/header/sort/index.ts b/apps/studio/components/grid/components/header/sort/index.ts deleted file mode 100644 index a28f916e587..00000000000 --- a/apps/studio/components/grid/components/header/sort/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as SortPopover } from './SortPopover' -export { default as SortPopoverPrimitive } from './SortPopoverPrimitive' diff --git a/apps/studio/components/grid/hooks/README.md b/apps/studio/components/grid/hooks/README.md index ffbb3c5d97f..559e08b58dc 100644 --- a/apps/studio/components/grid/hooks/README.md +++ b/apps/studio/components/grid/hooks/README.md @@ -46,35 +46,10 @@ The `useTableSort` hook manages sort state with these responsibilities: Key design aspects: -- Uses safe snapshot ONLY to get table name for formatting - Handles applying table name to sort objects - Maintains URL parameters as source of truth - Forwards sort changes to URL and triggers application-specific side effects -### `useSafeTableEditorSnapshot` - -```typescript -// Returns: Partial -``` - -This hook exists is to satisfy React's rules of hooks: - -React hooks CANNOT be called conditionally. Since `useTableSort` is a hook that needs table state, we cannot write: - -```typescript -// THIS VIOLATES REACT'S RULES - NOT ALLOWED -function useTableSort() { - const { sorts: urlSorts } = useTableEditorFiltersSort() - - // Conditional hook usage is forbidden in React! - const snap = hasTableContext ? useTableEditorTableStateSnapshot() : null - - // ...rest of hook -} -``` - -Instead, we created `useSafeTableEditorSnapshot` which unconditionally calls the original snapshot hook and handles error cases. This allows `useTableSort` to always call the hook safely regardless of context. - ## Component Implementation ### FilterPopoverPrimitive and SortPopoverPrimitive diff --git a/apps/studio/components/grid/hooks/useSaveTableEditorState.ts b/apps/studio/components/grid/hooks/useSaveTableEditorState.ts index 9620a166238..9157100956c 100644 --- a/apps/studio/components/grid/hooks/useSaveTableEditorState.ts +++ b/apps/studio/components/grid/hooks/useSaveTableEditorState.ts @@ -1,6 +1,7 @@ +import { useCallback } from 'react' + import { saveTableEditorStateToLocalStorage } from 'components/grid/SupabaseGrid.utils' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { useCallback } from 'react' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' /** @@ -8,30 +9,22 @@ import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' */ export function useSaveTableEditorState() { const { project } = useProjectContext() - // Use the safe snapshot hook - it returns a guaranteed object (real or default) const snap = useTableEditorTableStateSnapshot() const saveDataAndTriggerSideEffects = useCallback( (dataToSave: { filters?: string[]; sorts?: string[] }) => { const projectRef = project?.ref - // No need for top-level snap check, safeSnap always returns an object. if (!projectRef) { - console.warn( + return console.warn( '[useSaveTableEditorState] ProjectRef missing, cannot save or trigger side effects.' ) - return } - // --- Side Effects & Saving --- - // Call methods directly. If snap is the default object, these are safe no-ops. try { - // These calls are safe because defaultSnapshot provides no-op functions - snap.setPage?.(1) - snap.setEnforceExactCount?.(false) + snap.setPage(1) + snap.setEnforceExactCount(false) - // Local Storage Save - // Use optional chaining as table/name might be undefined in defaultSnapshot const tableName = snap.table?.name const schema = snap.table?.schema @@ -43,15 +36,13 @@ export function useSaveTableEditorState() { ...dataToSave, }) } else { - // Log if saving is skipped because table name is missing (likely due to default snap) console.warn('[useSaveTableEditorState] Table name missing, skipping localStorage save.') } } catch (error) { - // Catch unexpected errors during property access or method calls console.error('[useSaveTableEditorState] Error during interaction with snapshot:', error) } }, - [snap, project] // Depend on the safe snap and project + [snap, project] ) const saveFiltersAndTriggerSideEffects = useCallback( diff --git a/apps/studio/components/grid/hooks/useTableFilter.ts b/apps/studio/components/grid/hooks/useTableFilter.ts index 794963b4776..0181bbce9ca 100644 --- a/apps/studio/components/grid/hooks/useTableFilter.ts +++ b/apps/studio/components/grid/hooks/useTableFilter.ts @@ -1,7 +1,8 @@ import { useCallback } from 'react' + +import { filtersToUrlParams, formatFilterURLParams } from 'components/grid/SupabaseGrid.utils' import type { Filter } from 'components/grid/types' import { useTableEditorFiltersSort } from 'hooks/misc/useTableEditorFiltersSort' -import { filtersToUrlParams, formatFilterURLParams } from 'components/grid/SupabaseGrid.utils' import { useSaveTableEditorState } from './useSaveTableEditorState' /** @@ -18,7 +19,6 @@ export function useTableFilter() { (appliedFilters: Filter[]) => { const newUrlFilters = filtersToUrlParams(appliedFilters) setParams((prevParams) => ({ ...prevParams, filter: newUrlFilters })) - // Trigger save AND side effects using the dedicated hook saveFiltersAndTriggerSideEffects(newUrlFilters) }, [setParams, saveFiltersAndTriggerSideEffects] diff --git a/apps/studio/components/grid/hooks/useTableSort.ts b/apps/studio/components/grid/hooks/useTableSort.ts index ca3b2a7fb3e..e134e5be9ef 100644 --- a/apps/studio/components/grid/hooks/useTableSort.ts +++ b/apps/studio/components/grid/hooks/useTableSort.ts @@ -1,7 +1,8 @@ import { formatSortURLParams, sortsToUrlParams } from 'components/grid/SupabaseGrid.utils' +import { useCallback, useMemo } from 'react' + import type { Sort } from 'components/grid/types' import { useTableEditorFiltersSort } from 'hooks/misc/useTableEditorFiltersSort' -import { useCallback, useMemo } from 'react' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { useSaveTableEditorState } from './useSaveTableEditorState' @@ -17,9 +18,7 @@ export function useTableSort() { const snap = useTableEditorTableStateSnapshot() const { saveSortsAndTriggerSideEffects } = useSaveTableEditorState() - const tableName = useMemo(() => { - return snap.table?.name || '' - }, [snap]) + const tableName = useMemo(() => snap.table?.name || '', [snap]) const sorts = useMemo(() => { return formatSortURLParams(tableName, urlSorts) @@ -27,14 +26,13 @@ export function useTableSort() { const onApplySorts = useCallback( (appliedSorts: Sort[]) => { - const localTableName = snap.table?.name || '' - - if (!localTableName) { - console.warn('[useTableSort] Table name missing in callback, cannot apply sort correctly.') - return + if (!tableName) { + return console.warn( + '[useTableSort] Table name missing in callback, cannot apply sort correctly.' + ) } - const sortsWithTable = appliedSorts.map((sort) => ({ ...sort, table: localTableName })) + const sortsWithTable = appliedSorts.map((sort) => ({ ...sort, table: tableName })) const newUrlSorts = sortsToUrlParams(sortsWithTable) setParams((prevParams) => ({ ...prevParams, sort: newUrlSorts })) diff --git a/apps/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx b/apps/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx index 221e28ea833..e75da7acd71 100644 --- a/apps/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx +++ b/apps/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx @@ -1,24 +1,27 @@ import { Toggle } from 'ui' +import { useConsentState } from 'common' import Panel from 'components/ui/Panel' import { useSendResetMutation } from 'data/telemetry/send-reset-mutation' -import { useAppStateSnapshot } from 'state/app-state' export const AnalyticsSettings = () => { - const snap = useAppStateSnapshot() + const { hasAccepted, acceptAll, denyAll } = useConsentState() const { mutate: sendReset } = useSendResetMutation() const onToggleOptIn = () => { - const value = !snap.isOptedInTelemetry ? 'true' : 'false' - snap.setIsOptedInTelemetry(value === 'true') - if (value === 'false') sendReset() + if (hasAccepted) { + denyAll() + sendReset() + } else { + acceptAll() + } } return ( Analytics}> { const [mounted, setMounted] = useState(false) @@ -51,7 +51,7 @@ export const ThemeSettings = () => { > {singleThemes.map((theme: Theme) => ( { const { ref: projectRef } = useParams() @@ -199,7 +195,7 @@ const RateLimits = () => {

@@ -375,7 +371,7 @@ const RateLimits = () => {

Enable anonymous logins to update this rate limit

diff --git a/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx b/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx index 7314623ab07..1e4e15b6cac 100644 --- a/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx +++ b/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { toast } from 'sonner' import { Button, + cn, Collapsible, DropdownMenu, DropdownMenuContent, @@ -70,7 +71,7 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { canBypassRls, }} onSubmit={onSaveChanges} - className={[ + className={cn( 'bg-surface-100', 'hover:bg-overlay-hover', 'data-open:bg-selection', @@ -80,8 +81,8 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { '-space-y-px overflow-hidden', 'border border-t-0 first:border-t first:!mt-0 hover:border-t hover:-mt-[1px] shadow transition hover:z-50', 'first:rounded-tl first:rounded-tr', - 'last:rounded-bl last:rounded-br', - ].join(' ')} + 'last:rounded-bl last:rounded-br' + )} > {({ values, initialValues, handleReset }: any) => { const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) @@ -133,9 +134,7 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { {!disabled && ( - + - ) -}) - -const FunctionWithArguments = observer(({}) => { - const _localState = useContext(CreateTriggerContext) - - return ( - <> -
-
-
- -
-
-

{_localState!.formState.functionName.value}

-
- {_localState!.formState.functionSchema.value} -
-
-
- -
- - ) -}) diff --git a/apps/studio/components/interfaces/Database/Triggers/DeleteTrigger.tsx b/apps/studio/components/interfaces/Database/Triggers/DeleteTrigger.tsx index f2131e29369..d57da933a45 100644 --- a/apps/studio/components/interfaces/Database/Triggers/DeleteTrigger.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/DeleteTrigger.tsx @@ -1,3 +1,4 @@ +import { PostgresTrigger } from '@supabase/postgres-meta' import { toast } from 'sonner' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -5,12 +6,12 @@ import { useDatabaseTriggerDeleteMutation } from 'data/database-triggers/databas import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' interface DeleteTriggerProps { - trigger?: any + trigger?: PostgresTrigger visible: boolean setVisible: (value: boolean) => void } -const DeleteTrigger = ({ trigger, visible, setVisible }: DeleteTriggerProps) => { +export const DeleteTrigger = ({ trigger, visible, setVisible }: DeleteTriggerProps) => { const { project } = useProjectContext() const { id, name, schema } = trigger ?? {} @@ -45,7 +46,7 @@ const DeleteTrigger = ({ trigger, visible, setVisible }: DeleteTriggerProps) => loading={isLoading} confirmLabel={`Delete trigger ${name}`} confirmPlaceholder="Type in name of trigger" - confirmString={name} + confirmString={name ?? ''} text={ <> This will delete your trigger called{' '} @@ -59,5 +60,3 @@ const DeleteTrigger = ({ trigger, visible, setVisible }: DeleteTriggerProps) => /> ) } - -export default DeleteTrigger diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx new file mode 100644 index 00000000000..78b68d7f8ed --- /dev/null +++ b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx @@ -0,0 +1,448 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { Terminal } from 'lucide-react' +import { useEffect, useState } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'sonner' +import * as z from 'zod' + +import { PostgresTrigger } from '@supabase/postgres-meta' +import FormBoxEmpty from 'components/ui/FormBoxEmpty' +import { useDatabaseTriggerCreateMutation } from 'data/database-triggers/database-trigger-create-mutation' +import { useDatabaseTriggerUpdateMutation } from 'data/database-triggers/database-trigger-update-mutation' +import { useTablesQuery } from 'data/tables/tables-query' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { PROTECTED_SCHEMAS } from 'lib/constants/schemas' +import { + Button, + Checkbox_Shadcn_, + cn, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Input_Shadcn_, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + Separator, + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + SheetTitle, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import ChooseFunctionForm from './ChooseFunctionForm' +import { + TRIGGER_ENABLED_MODES, + TRIGGER_EVENTS, + TRIGGER_ORIENTATIONS, + TRIGGER_TYPES, +} from './Triggers.constants' + +const formId = 'create-trigger' + +const FormSchema = z.object({ + name: z + .string() + .min(1, 'Please provide a name for your trigger') + .regex(/^\S+$/, 'Name should not contain spaces or whitespaces'), + schema: z.string(), + table: z.string(), + activation: z.string(), + enabled_mode: z.string(), + orientation: z.string(), + function_name: z.string(), + function_schema: z.string(), + events: z.array(z.string()).min(1, 'Please select at least one event'), + + // For UI handling, not to be passed to the final request + tableId: z.string().optional(), +}) + +const defaultValues = { + name: '', + schema: '', + table: '', + activation: 'AFTER', + orientation: 'ROW', + function_name: '', + function_schema: '', + enabled_mode: 'ORIGIN', + events: [], +} + +interface TriggerSheetProps { + selectedTrigger?: PostgresTrigger + open: boolean + setOpen: (val: boolean) => void +} + +export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetProps) => { + const project = useSelectedProject() + + const [showFunctionSelector, setShowFunctionSelector] = useState(false) + + const { mutate: createDatabaseTrigger, isLoading: isCreating } = useDatabaseTriggerCreateMutation( + { + onSuccess: (res) => { + toast.success(`Successfully created trigger ${res.name}`) + setOpen(false) + }, + onError: (error) => { + toast.error(`Failed to create trigger: ${error.message}`) + }, + } + ) + const { mutate: updateDatabaseTrigger, isLoading: isUpdating } = useDatabaseTriggerUpdateMutation( + { + onSuccess: (res) => { + toast.success(`Successfully updated trigger ${res.name}`) + setOpen(false) + }, + onError: (error) => { + toast.error(`Failed to update trigger: ${error.message}`) + }, + } + ) + + const { data = [], isSuccess } = useTablesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const tables = data + .sort((a, b) => a.schema.localeCompare(b.schema)) + .filter((a) => !PROTECTED_SCHEMAS.includes(a.schema)) + const isEditing = !!selectedTrigger + + const form = useForm>({ + mode: 'onSubmit', + reValidateMode: 'onSubmit', + resolver: zodResolver(FormSchema), + defaultValues, + }) + const { function_name, function_schema } = form.watch() + + const onSubmit: SubmitHandler> = async (values) => { + if (!project) return console.error('Project is required') + const { tableId, ...payload } = values + + if (isEditing) { + updateDatabaseTrigger({ + projectRef: project?.ref, + connectionString: project?.connectionString, + id: selectedTrigger.id, + payload: { name: payload.name, enabled_mode: payload.enabled_mode }, + }) + } else { + createDatabaseTrigger({ + projectRef: project?.ref, + connectionString: project?.connectionString, + payload, + }) + } + } + + useEffect(() => { + if (open && isSuccess) { + form.clearErrors() + + if (isEditing) { + form.reset(selectedTrigger) + } else { + form.reset({ + ...defaultValues, + tableId: tables[0].id.toString(), + table: tables[0].name, + schema: tables[0].schema, + }) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, isSuccess]) + + return ( + <> + + + + Create a new database trigger + + + +
+ ( + + + + + + )} + /> + + {isEditing ? ( + ( + + + + + { + TRIGGER_ENABLED_MODES.find((option) => option.value === field.value) + ?.label + } + + + {TRIGGER_ENABLED_MODES.map((option) => ( + +

{option.label}

+

{option.description}

+
+ ))} +
+
+
+
+ )} + /> + ) : ( + <> + + + ( + + + { + const table = tables.find((x) => x.id.toString() === val) + if (table) { + form.setValue('table', table.name) + form.setValue('schema', table.schema) + } + }} + > + + + + + {tables.map((table) => ( + + {table.schema}. + {table.name} + + ))} + + + + + )} + /> + + ( + + {TRIGGER_EVENTS.map((event) => ( + ( + + + { + return checked + ? field.onChange([...field.value, event.value]) + : field.onChange( + field.value?.filter((value) => value !== event.value) + ) + }} + /> + + + )} + /> + ))} + + )} + /> + + ( + + + + + {TRIGGER_TYPES.find((option) => option.value === field.value)?.label} + + + {TRIGGER_TYPES.map((option) => ( + +

{option.label}

+

{option.description}

+
+ ))} +
+
+
+
+ )} + /> + + ( + + + + + { + TRIGGER_ORIENTATIONS.find((option) => option.value === field.value) + ?.label + } + + + {TRIGGER_ORIENTATIONS.map((option) => ( + +

{option.label}

+

{option.description}

+
+ ))} +
+
+
+
+ )} + /> + + + +
+

Function to trigger

+ {function_name.length === 0 ? ( + + ) : ( +
+
+
+ +
+

+ {function_schema} + .{function_name} +

+
+ +
+ )} +
+ + )} + +
+ + + + + +
+
+ + { + form.setValue('function_name', fn.name) + form.setValue('function_schema', fn.schema) + }} + /> + + ) +} diff --git a/apps/studio/components/interfaces/Database/Triggers/Triggers.constants.ts b/apps/studio/components/interfaces/Database/Triggers/Triggers.constants.ts new file mode 100644 index 00000000000..d0e194b9483 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Triggers/Triggers.constants.ts @@ -0,0 +1,46 @@ +export const TRIGGER_EVENTS = [ + { value: 'INSERT', label: 'Insert', description: 'Any insert operation on the table' }, + { + value: 'UPDATE', + label: 'Update', + description: 'Any update operation of any column on the table', + }, + { value: 'DELETE', label: 'Delete', description: 'Any delete operation of a record' }, +] + +export const TRIGGER_TYPES = [ + { + value: 'BEFORE', + label: 'Before the event', + description: 'Trigger fires before the operation is attempted', + }, + { + value: 'AFTER', + label: 'After the event', + description: 'Trigger fires before the operation has completed', + }, +] + +export const TRIGGER_ORIENTATIONS = [ + { + value: 'ROW', + label: 'Row', + description: 'Fires once for each processed row', + }, + { + value: 'STATEMENT', + label: 'Statement', + description: 'Fires once for each statement', + }, +] + +export const TRIGGER_ENABLED_MODES = [ + { value: 'ORIGIN', label: 'Origin', description: 'This is the default behaviour' }, + { + value: 'REPLICA', + label: 'Replica', + description: 'Will only fire if the session is in "replica" mode', + }, + { value: 'ALWAYS', label: 'Always', description: 'Will fire regardless of the replication role' }, + { value: 'DISABLED', label: 'Disabled', description: 'Will not fire' }, +] diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx index b7cc7a9c27f..0097ecc6907 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx @@ -1,6 +1,7 @@ +import { PostgresTrigger } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { noop, partition } from 'lodash' -import { Search } from 'lucide-react' +import { Plus, Search } from 'lucide-react' import { useState } from 'react' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -23,8 +24,8 @@ import TriggerList from './TriggerList' interface TriggersListProps { createTrigger: () => void - editTrigger: (trigger: any) => void - deleteTrigger: (trigger: any) => void + editTrigger: (trigger: PostgresTrigger) => void + deleteTrigger: (trigger: PostgresTrigger) => void } const TriggersList = ({ @@ -111,6 +112,7 @@ const TriggersList = ({
} onClick={() => createTrigger()} className="flex-grow" tooltip={{ @@ -122,8 +124,9 @@ const TriggersList = ({ }, }} > - Create a new trigger + New trigger + | 'ci_nano' @@ -28,6 +36,14 @@ export type ComputeInstanceSize = | '8XL' | '12XL' | '16XL' + | '24XL' + | '24XL - Optimized CPU' + | '24XL - Optimized Memory' + | '24XL - High Memory' + | '48XL' + | '48XL - Optimized CPU' + | '48XL - Optimized Memory' + | '48XL - High Memory' export type InfraInstanceSize = | 'pico' @@ -42,6 +58,14 @@ export type InfraInstanceSize = | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' export interface DiskManagementMessage { message: string diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts index bef3303bfca..a1040ca8b73 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts @@ -307,6 +307,14 @@ export const mapComputeSizeNameToAddonVariantId = ( '8xlarge': 'ci_8xlarge', '12xlarge': 'ci_12xlarge', '16xlarge': 'ci_16xlarge', + '24xlarge': 'ci_24xlarge', + '24xlarge_optimized_memory': 'ci_24xlarge_optimized_memory', + '24xlarge_optimized_cpu': 'ci_24xlarge_optimized_cpu', + '24xlarge_high_memory': 'ci_24xlarge_high_memory', + '48xlarge': 'ci_48xlarge', + '48xlarge_optimized_memory': 'ci_48xlarge_optimized_memory', + '48xlarge_optimized_cpu': 'ci_48xlarge_optimized_cpu', + '48xlarge_high_memory': 'ci_48xlarge_high_memory', }[computeSize ?? 'nano'] as ComputeInstanceAddonVariantId } @@ -325,6 +333,14 @@ export const mapAddOnVariantIdToComputeSize = ( ci_8xlarge: '8XL', ci_12xlarge: '12XL', ci_16xlarge: '16XL', + ci_24xlarge: '24XL', + ci_24xlarge_optimized_memory: '24XL - Optimized Memory', + ci_24xlarge_optimized_cpu: '24XL - Optimized CPU', + ci_24xlarge_high_memory: '24XL - High Memory', + ci_48xlarge: '48XL', + ci_48xlarge_optimized_memory: '48XL - Optimized Memory', + ci_48xlarge_optimized_cpu: '48XL - Optimized CPU', + ci_48xlarge_high_memory: '48XL - High Memory', }[addonVariantId] as ComputeInstanceSize } diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index 959f0677de6..e7c02e26881 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -29,6 +29,7 @@ import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { GB, PROJECT_STATUS } from 'lib/constants' +import { CloudProvider } from 'shared-data' import { Button, cn, @@ -55,7 +56,6 @@ import { DiskCountdownRadial } from './ui/DiskCountdownRadial' import { DiskType, RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3 } from './ui/DiskManagement.constants' import { NoticeBar } from './ui/NoticeBar' import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection' -import { CloudProvider } from 'shared-data' export function DiskManagementForm() { // isLoading is used to avoid a useCheckPermissions() race condition @@ -114,6 +114,8 @@ export function DiskManagementForm() { setRefetchInterval(false) toast.success('Disk configuration changes have been successfully applied!') } + } else { + setRefetchInterval(2000) } }, enabled: project != null && !isFlyArchitecture, diff --git a/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx b/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx index 7409aa2dbad..c3e151ef5ac 100644 --- a/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx +++ b/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx @@ -71,6 +71,14 @@ export const COMPUTE_BASELINE_IOPS = { ci_8xlarge: 40000, ci_12xlarge: 50000, ci_16xlarge: 80000, + ci_24xlarge: 120000, + ci_24xlarge_optimized_cpu: 120000, + ci_24xlarge_optimized_memory: 120000, + ci_24xlarge_high_memory: 120000, + ci_48xlarge: 240000, + ci_48xlarge_optimized_cpu: 240000, + ci_48xlarge_optimized_memory: 240000, + ci_48xlarge_high_memory: 240000, } export const COMPUTE_MAX_IOPS = { ci_nano: 11800, @@ -84,7 +92,16 @@ export const COMPUTE_MAX_IOPS = { ci_8xlarge: 40000, ci_12xlarge: 50000, ci_16xlarge: 80000, + ci_24xlarge: 120000, + ci_24xlarge_optimized_cpu: 120000, + ci_24xlarge_optimized_memory: 120000, + ci_24xlarge_high_memory: 120000, + ci_48xlarge: 240000, + ci_48xlarge_optimized_cpu: 240000, + ci_48xlarge_optimized_memory: 240000, + ci_48xlarge_high_memory: 240000, } + export const COMPUTE_BASELINE_THROUGHPUT = { ci_nano: 43, ci_micro: 87, @@ -97,7 +114,16 @@ export const COMPUTE_BASELINE_THROUGHPUT = { ci_8xlarge: 9500, ci_12xlarge: 14250, ci_16xlarge: 19000, + ci_24xlarge: 30000, + ci_24xlarge_optimized_cpu: 30000, + ci_24xlarge_optimized_memory: 30000, + ci_24xlarge_high_memory: 30000, + ci_48xlarge: 40000, + ci_48xlarge_optimized_cpu: 40000, + ci_48xlarge_optimized_memory: 40000, + ci_48xlarge_high_memory: 40000, } + export const COMPUTE_MAX_THROUGHPUT = { ci_nano: 2085, ci_micro: 2085, @@ -110,6 +136,14 @@ export const COMPUTE_MAX_THROUGHPUT = { ci_8xlarge: 9500, ci_12xlarge: 14250, ci_16xlarge: 19000, + ci_24xlarge: 30000, + ci_24xlarge_optimized_cpu: 30000, + ci_24xlarge_optimized_memory: 30000, + ci_24xlarge_high_memory: 30000, + ci_48xlarge: 40000, + ci_48xlarge_optimized_cpu: 40000, + ci_48xlarge_optimized_memory: 40000, + ci_48xlarge_high_memory: 40000, } export const RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3 = ['ci_nano', 'ci_micro', 'ci_small', 'ci_medium'] diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx index bfdd02cf96d..917a9cc87c6 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx @@ -79,10 +79,12 @@ const WrapperRow = ({ wrapper }: WrapperRowProps) => { /> - - - {table.schema}.{table.table_name} - + + + + {table.schema}.{table.table_name} + +
) })} diff --git a/apps/studio/components/interfaces/Linter/Linter.utils.tsx b/apps/studio/components/interfaces/Linter/Linter.utils.tsx index 872f90b3a85..234092e34fe 100644 --- a/apps/studio/components/interfaces/Linter/Linter.utils.tsx +++ b/apps/studio/components/interfaces/Linter/Linter.utils.tsx @@ -240,7 +240,7 @@ export const lintInfoMap: LintInfo[] = [ name: 'password_requirements_min_length', title: 'Minimum password length not set or inadequate', icon: , - link: ({ projectRef }) => `/project/${projectRef}/settings/auth`, + link: ({ projectRef }) => `/project/${projectRef}/auth/providers?provider=Email`, linkText: 'View settings', docsLink: 'https://supabase.com/docs/guides/platform/going-into-prod#security', category: 'security', @@ -258,7 +258,7 @@ export const lintInfoMap: LintInfo[] = [ name: 'auth_leaked_password_protection', title: 'Leaked Password Protection Disabled', icon: , - link: ({ projectRef }) => `/project/${projectRef}/settings/auth`, + link: ({ projectRef }) => `/project/${projectRef}/auth/providers?provider=Email`, linkText: 'View settings', docsLink: 'https://supabase.com/docs/guides/auth/password-security#password-strength-and-leaked-password-protection', @@ -268,7 +268,7 @@ export const lintInfoMap: LintInfo[] = [ name: 'auth_insufficient_mfa_options', title: 'Insufficient MFA Options', icon: , - link: ({ projectRef }) => `/project/${projectRef}/settings/auth`, + link: ({ projectRef }) => `/project/${projectRef}/auth/mfa`, linkText: 'View settings', docsLink: 'https://supabase.com/docs/guides/auth/auth-mfa', category: 'security', @@ -277,7 +277,7 @@ export const lintInfoMap: LintInfo[] = [ name: 'auth_password_policy_missing', title: 'Password Policy Missing', icon: , - link: ({ projectRef }) => `/project/${projectRef}/settings/auth`, + link: ({ projectRef }) => `/project/${projectRef}/auth/providers?provider=Email`, linkText: 'View settings', docsLink: 'https://supabase.com/docs/guides/auth/password-security', category: 'security', @@ -295,7 +295,7 @@ export const lintInfoMap: LintInfo[] = [ name: 'no_backup_admin', title: 'No Backup Admin Detected', icon: , - link: ({ projectRef }) => `/project/${projectRef}/settings/auth`, + link: ({ projectRef }) => `/project/${projectRef}/auth/mfa`, linkText: 'View settings', docsLink: 'https://supabase.com/docs/guides/auth/auth-mfa', category: 'security', diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx deleted file mode 100644 index 0275aaae5f7..00000000000 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' -import { useEffect } from 'react' -import { toast } from 'sonner' - -import { - ScaffoldSection, - ScaffoldSectionContent, - ScaffoldSectionDetail, -} from 'components/layouts/Scaffold' -import AlertError from 'components/ui/AlertError' -import { FormActions } from 'components/ui/Forms/FormActions' -import { FormPanel } from 'components/ui/Forms/FormPanel' -import { FormSection, FormSectionContent } from 'components/ui/Forms/FormSection' -import NoPermission from 'components/ui/NoPermission' -import PartnerManagedResource from 'components/ui/PartnerManagedResource' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' -import { useOrganizationCustomerProfileUpdateMutation } from 'data/organizations/organization-customer-profile-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { Form, Input, Listbox } from 'ui' -import { COUNTRIES } from './BillingAddress.constants' - -const BillingAddress = () => { - const { slug } = useParams() - const selectedOrganization = useSelectedOrganization() - - const canReadBillingAddress = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.customer' - ) - const canUpdateBillingAddress = useCheckPermissions( - PermissionAction.BILLING_WRITE, - 'stripe.customer' - ) - - const { data, error, isLoading, isSuccess, isError } = useOrganizationCustomerProfileQuery( - { slug }, - { enabled: canReadBillingAddress } - ) - const { mutate: updateCustomerProfile, isLoading: isUpdating } = - useOrganizationCustomerProfileUpdateMutation() - - const formId = 'billing-address-form' - const { city, country, line1, line2, postal_code, state } = data?.address ?? {} - const initialValues = { city, country, line1, line2, postal_code, state } - - const validate = (values: any) => { - const errors = {} as any - if ( - (values.line1 || values.line2 || values.postal_code || values.state || values.city) && - !values.country - ) { - errors['country'] = 'Please select a country' - } - if ( - (values.country || values.line2 || values.postal_code || values.state || values.city) && - !values.line1 - ) { - errors['line1'] = 'Please provide an address line' - } - return errors - } - - const onSubmit = async (values: any, { resetForm }: any) => { - if (!slug) return console.error('Slug is required') - - const address = !values.line1 ? null : values - - updateCustomerProfile( - { slug, address }, - { - onSuccess: () => { - toast.success('Successfully updated billing address') - resetForm({ values, initialValues: values }) - }, - } - ) - } - - return ( - - -
-

Billing Address

-

- This will be reflected in every upcoming invoice, past invoices are not affected -

-
-
- - {selectedOrganization?.managed_by !== undefined && - selectedOrganization?.managed_by !== 'supabase' ? ( - - ) : !canReadBillingAddress ? ( - - ) : ( - <> - {isLoading && ( -
- - - -
- )} - - {isError && ( - - )} - - {isSuccess && ( -
- {({ values, initialValues, handleReset, resetForm }: any) => { - const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) - - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - if (isSuccess && data !== undefined) { - const { city, country, line1, line2, postal_code, state } = - data?.address ?? {} - const values = { city, country, line1, line2, postal_code, state } - resetForm({ values, initialValues: values }) - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccess]) - - return ( - - -
- } - > - - - - -
- - - --- - - {COUNTRIES.map((country) => ( - - {country.name} - - ))} - - -
-
- - -
-
-
- - ) - }} - - )} - - )} - - - ) -} - -export default BillingAddress diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx index 28b5d774ba8..699e4b85330 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx @@ -11,19 +11,17 @@ import { import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import SparkBar from 'components/ui/SparkBar' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import UpcomingInvoice from './UpcomingInvoice' -import SparkBar from 'components/ui/SparkBar' const BillingBreakdown = () => { const { slug: orgSlug } = useParams() - const canReadSubscriptions = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.subscriptions' - ) + const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') const { data: subscription, @@ -74,7 +72,7 @@ const BillingBreakdown = () => {
- {!canReadSubscriptions ? ( + {isPermissionsLoaded && !canReadSubscriptions ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.constants.ts b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingAddress.constants.ts similarity index 100% rename from apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.constants.ts rename to apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingAddress.constants.ts diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx new file mode 100644 index 00000000000..5082e90df99 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx @@ -0,0 +1,178 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useMemo, useState } from 'react' +import { toast } from 'sonner' + +import { useParams } from 'common' +import { + ScaffoldSection, + ScaffoldSectionContent, + ScaffoldSectionDetail, +} from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' +import NoPermission from 'components/ui/NoPermission' +import PartnerManagedResource from 'components/ui/PartnerManagedResource' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' +import { useOrganizationCustomerProfileUpdateMutation } from 'data/organizations/organization-customer-profile-update-mutation' +import { useOrganizationTaxIdQuery } from 'data/organizations/organization-tax-id-query' +import { useOrganizationTaxIdUpdateMutation } from 'data/organizations/organization-tax-id-update-mutation' +import { + useAsyncCheckProjectPermissions, + useCheckPermissions, +} from 'hooks/misc/useCheckPermissions' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { Button, Card, CardFooter, Form_Shadcn_ as Form } from 'ui' +import { BillingCustomerDataForm } from './BillingCustomerDataForm' +import { TAX_IDS } from './TaxID.constants' +import { useBillingCustomerDataForm } from './useBillingCustomerDataForm' + +export const BillingCustomerData = () => { + const { slug } = useParams() + const selectedOrganization = useSelectedOrganization() + + const { isSuccess: isPermissionsLoaded, can: canReadBillingCustomerData } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.customer') + const canUpdateBillingCustomerData = useCheckPermissions( + PermissionAction.BILLING_WRITE, + 'stripe.customer' + ) + + const { + data: customerProfile, + error, + isLoading, + isSuccess, + } = useOrganizationCustomerProfileQuery({ slug }, { enabled: canReadBillingCustomerData }) + + const { + data: taxId, + error: errorLoadingTaxId, + isLoading: isLoadingTaxId, + isSuccess: loadedTaxId, + } = useOrganizationTaxIdQuery({ slug }) + + const initialCustomerData = useMemo( + () => ({ + ...customerProfile?.address, + billing_name: customerProfile?.billing_name, + tax_id_type: taxId?.type, + tax_id_value: taxId?.value, + tax_id_name: taxId + ? TAX_IDS.find( + (option) => option.type === taxId.type && option.countryIso2 === taxId.country + )?.name || '' + : '', + }), + [customerProfile, taxId] + ) + + const { mutateAsync: updateCustomerProfile } = useOrganizationCustomerProfileUpdateMutation() + const { mutateAsync: updateTaxId } = useOrganizationTaxIdUpdateMutation() + + const [isSubmitting, setIsSubmitting] = useState(false) + + const { form, handleSubmit, handleReset, isDirty } = useBillingCustomerDataForm({ + initialCustomerData, + onCustomerDataChange: async (data) => { + setIsSubmitting(true) + + try { + await updateCustomerProfile({ + slug, + address: data.address, + billing_name: data.billing_name, + }) + + await updateTaxId({ slug, taxId: data.tax_id }) + + toast.success('Successfully updated billing data') + + setIsSubmitting(false) + } catch (error: any) { + toast.error(`Failed updating billing data: ${error.message}`) + setIsSubmitting(false) + } + }, + }) + + const isSubmitDisabled = !isDirty || !canUpdateBillingCustomerData || isSubmitting + + return ( + + +
+

Billing Address & Tax ID

+

+ This will be reflected in every upcoming invoice, past invoices are not affected +

+
+
+ + {selectedOrganization?.managed_by !== undefined && + selectedOrganization?.managed_by !== 'supabase' ? ( + + ) : isPermissionsLoaded && !canReadBillingCustomerData ? ( + + ) : ( + <> + {(isLoading || isLoadingTaxId) && ( +
+ + + +
+ )} + + {(error || errorLoadingTaxId) && ( + + )} + + {isSuccess && loadedTaxId && ( + +
+ + + + {!canUpdateBillingCustomerData && ( + + You need additional permissions to manage this organization's billing + address + + )} +
+ + +
+
+ + +
+ )} + + )} +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataExistingOrgDialog.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataExistingOrgDialog.tsx new file mode 100644 index 00000000000..9fac673f8fc --- /dev/null +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataExistingOrgDialog.tsx @@ -0,0 +1,215 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { Pencil } from 'lucide-react' +import { useMemo, useState } from 'react' +import { toast } from 'sonner' + +import AlertError from 'components/ui/AlertError' +import NoPermission from 'components/ui/NoPermission' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' +import { useOrganizationCustomerProfileUpdateMutation } from 'data/organizations/organization-customer-profile-update-mutation' +import { useOrganizationTaxIdQuery } from 'data/organizations/organization-tax-id-query' +import { useOrganizationTaxIdUpdateMutation } from 'data/organizations/organization-tax-id-update-mutation' +import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogSectionSeparator, + DialogTitle, + Form_Shadcn_ as Form, + Label_Shadcn_ as Label, +} from 'ui' +import { BillingCustomerDataForm } from './BillingCustomerDataForm' +import { TAX_IDS } from './TaxID.constants' +import { useBillingCustomerDataForm } from './useBillingCustomerDataForm' + +export const BillingCustomerDataExistingOrgDialog = () => { + const [open, setOpen] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + + const { slug } = useSelectedOrganization() ?? {} + + const canReadBillingCustomerData = useCheckPermissions( + PermissionAction.BILLING_READ, + 'stripe.customer' + ) + + const canUpdateBillingCustomerData = useCheckPermissions( + PermissionAction.BILLING_WRITE, + 'stripe.customer' + ) + + const { + data: customerProfile, + error, + isLoading, + isSuccess, + } = useOrganizationCustomerProfileQuery({ slug }, { enabled: canReadBillingCustomerData }) + + const { + data: taxId, + error: errorTaxId, + isLoading: isLoadingTaxId, + isSuccess: isSuccessTaxId, + } = useOrganizationTaxIdQuery({ slug }) + + const initialCustomerData = useMemo( + () => ({ + ...customerProfile?.address, + billing_name: customerProfile?.billing_name, + tax_id_type: taxId?.type, + tax_id_value: taxId?.value, + tax_id_name: taxId + ? TAX_IDS.find( + (option) => option.type === taxId.type && option.countryIso2 === taxId.country + )?.name || '' + : '', + }), + [customerProfile, taxId] + ) + + const { mutateAsync: updateCustomerProfile } = useOrganizationCustomerProfileUpdateMutation() + const { mutateAsync: updateTaxId } = useOrganizationTaxIdUpdateMutation() + + const { form, handleSubmit, handleReset, isDirty } = useBillingCustomerDataForm({ + initialCustomerData, + onCustomerDataChange: async (data) => { + setIsSubmitting(true) + + try { + await updateCustomerProfile({ + slug, + address: data.address ?? undefined, + billing_name: data.billing_name, + }) + + await updateTaxId({ slug, taxId: data.tax_id }) + + toast.success('Successfully updated billing data') + + setOpen(false) + + setIsSubmitting(false) + } catch (error: any) { + toast.error(`Failed updating billing data: ${error.message}`) + setIsSubmitting(false) + } + }, + }) + + const handleClose = () => { + handleReset() + setOpen(false) + } + + const getAddressSummary = () => { + if (!customerProfile?.address?.line1) return 'Optionally add a billing address' + + const parts = [ + customerProfile?.billing_name, + customerProfile?.address?.line1, + customerProfile?.address?.city, + customerProfile?.address?.state, + customerProfile?.address?.country, + ].filter(Boolean) + + return parts.join(', ') + } + + const isSubmitDisabled = !isDirty || !canUpdateBillingCustomerData || isSubmitting + + return ( + <> +
+ + {!canReadBillingCustomerData ? ( + + ) : ( + <> + {(isLoading || isLoadingTaxId) && ( +
+ +
+ )} + {(error || errorTaxId) && ( + + )} + {isSuccess && isSuccessTaxId && ( +
+

{getAddressSummary()}

+ +
+ )} + + )} +
+ + { + if (!value) setOpen(false) + else setOpen(true) + }} + > + + + Billing Address & Tax Id + + This will be reflected in every upcoming invoice, past invoices are not affected + + + +
+ + + + {!canUpdateBillingCustomerData && ( + + You need additional permissions to manage this organization's billing address + + )} +
+ + +
+
+ + +
+
+ + ) +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx new file mode 100644 index 00000000000..7b7a8da1333 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx @@ -0,0 +1,338 @@ +import { Check, ChevronsUpDown, X } from 'lucide-react' +import { UseFormReturn } from 'react-hook-form' +import { z } from 'zod' + +import { useState } from 'react' +import { + Button, + cn, + Command_Shadcn_ as Command, + CommandEmpty_Shadcn_ as CommandEmpty, + CommandGroup_Shadcn_ as CommandGroup, + CommandInput_Shadcn_ as CommandInput, + CommandItem_Shadcn_ as CommandItem, + CommandList_Shadcn_ as CommandList, + FormControl_Shadcn_ as FormControl, + FormField_Shadcn_ as FormField, + FormMessage_Shadcn_ as FormMessage, + Input_Shadcn_ as Input, + Input_Shadcn_, + Popover_Shadcn_ as Popover, + PopoverContent_Shadcn_ as PopoverContent, + PopoverTrigger_Shadcn_ as PopoverTrigger, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { COUNTRIES } from './BillingAddress.constants' +import { TAX_IDS } from './TaxID.constants' + +interface BillingCustomerDataFormProps { + form: UseFormReturn + disabled?: boolean + className?: string +} + +// Define the expected form values structure and validation schema +export const BillingCustomerDataSchema = z + .object({ + billing_name: z.string().min(3, 'Name must be at least 3 letters long'), + line1: z.string().optional(), + line2: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + postal_code: z.string().optional(), + country: z.string().optional(), + tax_id_type: z.string(), + tax_id_value: z.string(), + tax_id_name: z.string(), + }) + .refine( + (data) => { + // its fine to just set the name, but once any other field is set, requires full address + const hasAnyField = data.line1 || data.line2 || data.city || data.state || data.postal_code + // If any field has value, country and line1 must have values. + return !hasAnyField || (!!data.country && !!data.line1) + }, + { + message: 'Country and Address line 1 are required if any other field is provided.', + path: ['line1'], + } + ) + .refine((data) => !(!!data.line1 && !data.country), { + message: 'Please select a country', + path: ['country'], + }) + .refine((data) => !(!!data.country && !data.line1), { + message: 'Please provide an address line 1', + path: ['line1'], + }) + +export type BillingCustomerDataFormValues = z.infer + +export const BillingCustomerDataForm = ({ + form, + disabled = false, + className, +}: BillingCustomerDataFormProps) => { + const [showCountriesPopover, setShowCountriesPopover] = useState(false) + const [showTaxIDsPopover, setShowTaxIDsPopover] = useState(false) + + const onSelectTaxIdType = (name: string) => { + const selectedTaxIdOption = TAX_IDS.find((option) => option.name === name) + if (!selectedTaxIdOption) return + form.setValue('tax_id_type', selectedTaxIdOption.type) + form.setValue('tax_id_value', '') + form.setValue('tax_id_name', name) + } + + const onRemoveTaxId = () => { + form.setValue('tax_id_name', '', { shouldDirty: true }) + form.setValue('tax_id_type', '', { shouldDirty: true }) + form.setValue('tax_id_value', '', { shouldDirty: true }) + } + + const { tax_id_name } = form.watch() + const selectedTaxId = TAX_IDS.find((option) => option.name === tax_id_name) + + return ( +
+ ( + + + + + + + )} + /> + + ( + + + + + + + )} + /> + + ( + + + + + + + )} + /> + +
+ ( + + + + + + + + + + + + No country found. + + {COUNTRIES.map((country) => ( + { + form.setValue('country', country.code, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }) + setShowCountriesPopover(false) + }} + > + + {country.name} + + ))} + + + + + + + + )} + /> + ( + + + + + + + )} + /> +
+ +
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> +
+ +
+ ( + + + + + + + + + + + + No tax ID found. + + {TAX_IDS.sort((a, b) => a.country.localeCompare(b.country)).map( + (option) => ( + { + onSelectTaxIdType(option.name) + setShowTaxIDsPopover(false) + }} + > + + {option.country} - {option.name} + + ) + )} + + + + + + + + )} + /> + + {selectedTaxId && ( +
+ ( + + + + + + )} + /> + +
+ )} +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataNewOrgDialog.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataNewOrgDialog.tsx new file mode 100644 index 00000000000..dd072469401 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataNewOrgDialog.tsx @@ -0,0 +1,122 @@ +import { FocusTrap } from '@headlessui/react' +import { Pencil } from 'lucide-react' +import { useState } from 'react' + +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogSectionSeparator, + DialogTitle, + DialogTrigger, + Form_Shadcn_ as Form, +} from 'ui' +import { BillingCustomerDataForm } from './BillingCustomerDataForm' +import { FormCustomerData, useBillingCustomerDataForm } from './useBillingCustomerDataForm' + +interface BillingCustomerDataNewOrgDialogProps { + onCustomerDataChange: (data: FormCustomerData) => void +} + +export const BillingCustomerDataNewOrgDialog = ({ + onCustomerDataChange, +}: BillingCustomerDataNewOrgDialogProps) => { + const [open, setOpen] = useState(false) + + const handleDialogClose = () => { + setOpen(false) + } + + const { form, handleSubmit, handleReset, isDirty } = useBillingCustomerDataForm({ + onCustomerDataChange, + }) + + const handleClose = () => { + handleReset() + handleDialogClose() + } + + const getAddressSummary = () => { + const { line1, city, state, country, billing_name } = form.getValues() + + if (!line1) return 'Optionally add a billing address' + + const parts = [billing_name, line1, city, state, country].filter(Boolean) + + return parts.join(', ') + } + + const isSubmitDisabled = !isDirty + + return ( +
+ + { + if (!value) handleDialogClose() + else setOpen(true) + }} + > + + + + + + Billing Address & Tax Id + + + {/* + [Joshen] There's something odd going on with using Dialog and RHF FormField here + where the focus keeps going to the Dialog when tabbing across the input fields, hence the FocusTrap + What's weirder is that once you've cycled the focus through all the inputs at least once, the focus + then no longer goes to the Dialog thereafter + FWIW it's likely happening across the dashboard whereever we're using Dialog and FormField + */} + +
+ { + e.preventDefault() + e.stopPropagation() + form.handleSubmit(handleSubmit)(e) + + // Only close the dialog if the form is valid + if (!Object.keys(form.formState.errors).length) { + handleDialogClose() + } + }} + > + + +
+ + +
+
+ + +
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.constants.ts similarity index 100% rename from apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts rename to apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.constants.ts diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.utils.ts similarity index 100% rename from apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts rename to apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.utils.ts diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm.ts b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm.ts new file mode 100644 index 00000000000..aef1e286377 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm.ts @@ -0,0 +1,131 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useEffect } from 'react' +import { useForm } from 'react-hook-form' + +import { BillingCustomerDataFormValues, BillingCustomerDataSchema } from './BillingCustomerDataForm' +import { TAX_IDS } from './TaxID.constants' +import { sanitizeTaxIdValue } from './TaxID.utils' +import { components } from 'api-types' + +export type FormCustomerData = { + address: components['schemas']['CustomerResponse']['address'] | undefined + billing_name: string + tax_id: components['schemas']['TaxIdResponse']['tax_id'] | null +} + +interface UseBillingAddressFormProps { + initialCustomerData?: Partial | null + onCustomerDataChange: (data: FormCustomerData) => void +} + +export function useBillingCustomerDataForm({ + initialCustomerData, + onCustomerDataChange, +}: UseBillingAddressFormProps) { + const form = useForm({ + resolver: zodResolver(BillingCustomerDataSchema), + defaultValues: { + billing_name: initialCustomerData?.billing_name || '', + line1: initialCustomerData?.line1 || '', + line2: initialCustomerData?.line2 || '', + city: initialCustomerData?.city || '', + state: initialCustomerData?.state || '', + postal_code: initialCustomerData?.postal_code || '', + country: initialCustomerData?.country || '', + tax_id_type: initialCustomerData?.tax_id_type || '', + tax_id_value: initialCustomerData?.tax_id_value || '', + tax_id_name: initialCustomerData?.tax_id_name || '', + }, + }) + + // Update form when initialAddress changes + useEffect(() => { + if (initialCustomerData) { + form.reset({ + billing_name: initialCustomerData.billing_name || '', + line1: initialCustomerData.line1 || '', + line2: initialCustomerData.line2 || '', + city: initialCustomerData.city || '', + state: initialCustomerData.state || '', + postal_code: initialCustomerData.postal_code || '', + country: initialCustomerData.country || '', + tax_id_type: initialCustomerData?.tax_id_type || '', + tax_id_value: initialCustomerData?.tax_id_value || '', + tax_id_name: initialCustomerData?.tax_id_name || '', + }) + } + }, [initialCustomerData]) + + const handleSubmit = async (values: BillingCustomerDataFormValues) => { + const trimmedValues = Object.entries(values).reduce((acc, [key, value]) => { + acc[key as keyof BillingCustomerDataFormValues] = + typeof value === 'string' ? value.trim() : value + return acc + }, {} as BillingCustomerDataFormValues) + + const addressPayload = !trimmedValues.line1 ? null : trimmedValues + + const selectedTaxId = TAX_IDS.find((option) => option.name === values.tax_id_name) + + onCustomerDataChange({ + address: addressPayload + ? { + line1: trimmedValues.line1!, + line2: trimmedValues.line2, + city: trimmedValues.city, + state: trimmedValues.state, + postal_code: trimmedValues.postal_code, + country: trimmedValues.country!, + } + : undefined, + billing_name: trimmedValues.billing_name, + tax_id: + selectedTaxId && values.tax_id_type?.length && values.tax_id_value?.length + ? { + type: values.tax_id_type, + value: sanitizeTaxIdValue({ + value: values.tax_id_value, + name: form.getValues().tax_id_name, + }), + country: selectedTaxId?.countryIso2, + } + : null, + }) + } + + const handleReset = () => { + if (initialCustomerData) { + form.reset({ + billing_name: initialCustomerData.billing_name || '', + line1: initialCustomerData.line1 || '', + line2: initialCustomerData.line2 || '', + city: initialCustomerData.city || '', + state: initialCustomerData.state || '', + postal_code: initialCustomerData.postal_code || '', + country: initialCustomerData.country || '', + tax_id_type: initialCustomerData?.tax_id_type || '', + tax_id_value: initialCustomerData?.tax_id_value || '', + tax_id_name: initialCustomerData?.tax_id_name || '', + }) + } else { + form.reset({ + line1: '', + line2: '', + city: '', + state: '', + postal_code: '', + country: '', + tax_id_type: '', + tax_id_value: '', + tax_id_name: '', + }) + } + } + + return { + form, + handleSubmit, + handleReset, + isDirty: form.formState.isDirty, + } +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx index 0ee67a41a87..8af7d72ade4 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx @@ -18,7 +18,10 @@ import { FormSection, FormSectionContent } from 'components/ui/Forms/FormSection import NoPermission from 'components/ui/NoPermission' import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' import { useOrganizationUpdateMutation } from 'data/organizations/organization-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { + useAsyncCheckProjectPermissions, + useCheckPermissions, +} from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { FormMessage_Shadcn_, Input_Shadcn_ } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -43,10 +46,8 @@ const BillingEmail = () => { const { name, billing_email } = selectedOrganization ?? {} const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') - const canReadBillingEmail = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.subscriptions' - ) + const { isSuccess: isPermissionsLoaded, can: canReadBillingEmail } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') const { data: billingCustomer, isLoading: loadingBillingCustomer } = useOrganizationCustomerProfileQuery({ slug }, { enabled: canReadBillingEmail }) @@ -107,7 +108,7 @@ const BillingEmail = () => {
- {!canReadBillingEmail ? ( + {isPermissionsLoaded && !canReadBillingEmail ? ( ) : (
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx index 0c78de65c91..3109fc69ada 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx @@ -5,19 +5,18 @@ import { ScaffoldDivider, ScaffoldTitle, } from 'components/layouts/Scaffold' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { cn } from 'ui' -import { useOrgSubscriptionQuery } from '../../../../data/subscriptions/org-subscription-query' import InvoicesSection from '../InvoicesSettings/InvoicesSection' -import BillingAddress from './BillingAddress/BillingAddress' import BillingBreakdown from './BillingBreakdown/BillingBreakdown' +import { BillingCustomerData } from './BillingCustomerData/BillingCustomerData' import BillingEmail from './BillingEmail' import CostControl from './CostControl/CostControl' import CreditBalance from './CreditBalance' import PaymentMethods from './PaymentMethods/PaymentMethods' import Subscription from './Subscription/Subscription' -import TaxID from './TaxID/TaxID' const BillingSettings = () => { const { @@ -109,13 +108,7 @@ const BillingSettings = () => { - - - - - - - + )} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx index 4b63791d318..fc8b417481f 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx @@ -14,7 +14,7 @@ import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useFlag } from 'hooks/ui/useFlag' import { BASE_PATH } from 'lib/constants' import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings' @@ -28,10 +28,8 @@ const CostControl = ({}: CostControlProps) => { const { slug } = useParams() const { resolvedTheme } = useTheme() - const canReadSubscriptions = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.subscriptions' - ) + const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') const snap = useOrgSettingsPageStateSnapshot() const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') @@ -85,7 +83,7 @@ const CostControl = ({}: CostControlProps) => {
- {!canReadSubscriptions ? ( + {isPermissionsLoaded && !canReadSubscriptions ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx index ab0cfee3adc..11e8d02f1be 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx @@ -11,7 +11,7 @@ import { FormPanel } from 'components/ui/Forms/FormPanel' import { FormSection, FormSectionContent } from 'components/ui/Forms/FormSection' import NoPermission from 'components/ui/NoPermission' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useFlag } from 'hooks/ui/useFlag' import { CreditTopUp } from './CreditTopUp' @@ -19,10 +19,8 @@ const CreditBalance = () => { const { slug } = useParams() const creditTopUpEnabled = useFlag('creditTopUp') - const canReadSubscriptions = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.subscriptions' - ) + const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') const { data: subscription, @@ -54,7 +52,7 @@ const CreditBalance = () => {
- {!canReadSubscriptions ? ( + {isPermissionsLoaded && !canReadSubscriptions ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx index 788707ffb5b..15b85fa5ecf 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx @@ -19,7 +19,10 @@ import PartnerManagedResource from 'components/ui/PartnerManagedResource' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrganizationPaymentMethodsQuery } from 'data/organizations/organization-payment-methods-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { + useAsyncCheckProjectPermissions, + useCheckPermissions, +} from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { getURL } from 'lib/helpers' import { Alert, Button } from 'ui' @@ -43,10 +46,8 @@ const PaymentMethods = () => { isSuccess, } = useOrganizationPaymentMethodsQuery({ slug }) - const canReadPaymentMethods = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.payment_methods' - ) + const { isSuccess: isPermissionsLoaded, can: canReadPaymentMethods } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.payment_methods') const canUpdatePaymentMethods = useCheckPermissions( PermissionAction.BILLING_WRITE, 'stripe.payment_methods' @@ -73,7 +74,7 @@ const PaymentMethods = () => { installationId: selectedOrganization?.partner_id, }} /> - ) : !canReadPaymentMethods ? ( + ) : isPermissionsLoaded && !canReadPaymentMethods ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx index 89e9f0af6e6..a153e2cd2f8 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx @@ -26,9 +26,8 @@ import DowngradeModal from './DowngradeModal' import { EnterpriseCard } from './EnterpriseCard' import ExitSurveyModal from './ExitSurveyModal' import MembersExceedLimitModal from './MembersExceedLimitModal' -import SubscriptionPlanUpdateDialog from './SubscriptionPlanUpdateDialog' +import { SubscriptionPlanUpdateDialog } from './SubscriptionPlanUpdateDialog' import UpgradeSurveyModal from './UpgradeModal' -import { PROJECT_STATUS } from 'lib/constants' const PlanUpdateSidePanel = () => { const router = useRouter() @@ -305,7 +304,6 @@ const PlanUpdateSidePanel = () => { billingPartner={billingPartner} subscription={subscription} projects={orgProjects} - slug={slug} currentPlanMeta={{ ...availablePlans.find((p) => p.id === subscription?.plan?.id), features: diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx index 919f88ef2c0..3c31b9a18c1 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx @@ -11,7 +11,7 @@ import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useFlag } from 'hooks/ui/useFlag' import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings' import { Alert, Button } from 'ui' @@ -25,10 +25,8 @@ const Subscription = () => { const snap = useOrgSettingsPageStateSnapshot() const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') - const canReadSubscriptions = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.subscriptions' - ) + const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') const { data: subscription, @@ -61,7 +59,7 @@ const Subscription = () => { - {!canReadSubscriptions ? ( + {isPermissionsLoaded && !canReadSubscriptions ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx index 51d91402ff9..96349509809 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx @@ -1,5 +1,5 @@ import { useQueryClient } from '@tanstack/react-query' -import { Check, ExternalLink, InfoIcon } from 'lucide-react' +import { Check, InfoIcon } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import tweets from 'shared-data/tweets' @@ -10,29 +10,17 @@ import AlertError from 'components/ui/AlertError' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { organizationKeys } from 'data/organizations/keys' import { OrganizationBillingSubscriptionPreviewResponse } from 'data/organizations/organization-billing-subscription-preview' +import { ProjectInfo } from 'data/projects/projects-query' import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation' import { SubscriptionTier } from 'data/subscriptions/types' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { PRICING_TIER_PRODUCT_IDS, PROJECT_STATUS } from 'lib/constants' import { formatCurrency } from 'lib/helpers' -import { - Badge, - Button, - Card, - CardContent, - Dialog, - DialogContent, - HoverCard, - HoverCardContent, - HoverCardTrigger, - Table, - TableBody, - TableCell, - TableRow, -} from 'ui' -import { InfoTooltip } from 'ui-patterns/info-tooltip' -import PaymentMethodSelection from './PaymentMethodSelection' -import { ProjectInfo } from 'data/projects/projects-query' +import { Badge, Button, Dialog, DialogContent, Table, TableBody, TableCell, TableRow } from 'ui' import { Admonition } from 'ui-patterns' +import { InfoTooltip } from 'ui-patterns/info-tooltip' +import { BillingCustomerDataExistingOrgDialog } from '../BillingCustomerData/BillingCustomerDataExistingOrgDialog' +import PaymentMethodSelection from './PaymentMethodSelection' const getRandomTweet = () => { const filteredTweets = tweets.filter((it) => it.text.length < 180) @@ -71,12 +59,11 @@ interface Props { billingPartner?: string selectedOrganization: any subscription: any - slug?: string currentPlanMeta: any projects: ProjectInfo[] } -const SubscriptionPlanUpdateDialog = ({ +export const SubscriptionPlanUpdateDialog = ({ selectedTier, onClose, subscriptionPlanMeta, @@ -89,11 +76,11 @@ const SubscriptionPlanUpdateDialog = ({ billingPartner, selectedOrganization, subscription, - slug, currentPlanMeta, projects, }: Props) => { const queryClient = useQueryClient() + const { slug } = useSelectedOrganization() ?? {} const [selectedPaymentMethod, setSelectedPaymentMethod] = useState() const [testimonialTweet, setTestimonialTweet] = useState(getRandomTweet()) @@ -167,6 +154,24 @@ const SubscriptionPlanUpdateDialog = ({ }) : [] + // Calculate remaining days in current billing cycle + const now = Math.floor(Date.now() / 1000) // current time in seconds + const remainingSeconds = subscription?.current_period_end - now + const totalSeconds = subscription?.current_period_end - subscription?.current_period_start + const remainingRatio = remainingSeconds / totalSeconds + + // Calculate prorated credit for current plan + const currentPlanMonthlyPrice = currentPlanMeta?.price ?? 0 + const proratedCredit = currentPlanMonthlyPrice * remainingRatio + + // Calculate new plan cost + const newPlanCost = subscriptionPlanMeta?.priceMonthly ?? 0 + + const customerBalance = ((subscription?.customer_balance ?? 0) / 100) * -1 + + // Calculate total charge (new plan - prorated credit) + const totalCharge = Math.max(0, newPlanCost - proratedCredit - customerBalance) + return ( - - - {(() => { - // Calculate remaining days in current billing cycle - const now = Math.floor(Date.now() / 1000) // current time in seconds - const remainingSeconds = subscription?.current_period_end - now - const totalSeconds = - subscription?.current_period_end - subscription?.current_period_start - const remainingRatio = remainingSeconds / totalSeconds - - // Calculate prorated credit for current plan - const currentPlanMonthlyPrice = currentPlanMeta?.price ?? 0 - const proratedCredit = currentPlanMonthlyPrice * remainingRatio - - // Calculate new plan cost - const newPlanCost = subscriptionPlanMeta?.priceMonthly ?? 0 - - const customerBalance = ((subscription?.customer_balance ?? 0) / 100) * -1 - - // Calculate total charge (new plan - prorated credit) - const totalCharge = Math.max( - 0, - newPlanCost - proratedCredit - customerBalance - ) - - return ( - <> - - - {subscriptionPlanMeta?.name} Plan - - New - - - - {formatCurrency(newPlanCost)} - - - {subscription?.plan?.id !== 'free' && ( - - - Unused Time on {subscription?.plan?.name} Plan - - Your previous plan was charged upfront, so a plan change will - prorate any unused time in credits. If the prorated credits - exceed the new plan charge, the excessive credits are added to - your organization for future use. - - - - -{formatCurrency(proratedCredit)} - - - )} - - {/* Ignore rare case with negative balance (debt) */} - {customerBalance > 0 && ( - - - Credits - - Credits will be used first before charging your card. - - - - {formatCurrency(customerBalance)} - - - )} - - Charge today - - {formatCurrency(totalCharge)} - - - {subscription?.plan?.id !== 'free' && ( - - - + current cycle usage - - Changing your plan resets your billing cycle. If your previous - billing cycle still has any outstanding usage charges, they will - be added to your total charge today. - - - - - View spend - - - - )} - - ) - })()} - -
- - - - - - Monthly invoice estimate is{' '} - {formatCurrency( - Math.round( - subscriptionPreview?.breakdown.reduce( - (prev: number, cur) => prev + cur.total_price, - 0 - ) ?? 0 - ) - )} - - - - -

Your new monthly invoice

- - {subscriptionPreviewError && ( - - )} - - {subscriptionPreviewIsLoading && ( -
- Estimating monthly costs... - - - +
+
+
+ {subscriptionPlanMeta?.name} Plan + + New + +
+
{formatCurrency(newPlanCost)}
+
+ {subscription?.plan?.id !== 'free' && ( +
+
+ Unused Time on {subscription?.plan?.name} Plan + + Your previous plan was charged upfront, so a plan change will prorate + any unused time in credits. If the prorated credits exceed the new plan + charge, the excessive credits are added to your organization for future + use. +
- )} +
+ -{formatCurrency(proratedCredit)} +
+
+ )} - {subscriptionPreviewInitialized && ( - <> - - - {/* Non-compute items and Projects list */} - {(() => { - // Combine all compute-related projects - const computeItems = - subscriptionPreview?.breakdown?.filter( - (item) => - item.description?.toLowerCase().includes('compute') && - item.breakdown?.length > 0 - ) || [] + {/* Ignore rare case with negative balance (debt) */} + {customerBalance > 0 && ( +
+
+ Credits + + Credits will be used first before charging your card. + +
+
+ {formatCurrency(customerBalance)} +
+
+ )} +
+
Charge today
+
+ {formatCurrency(totalCharge)} + {subscription?.plan?.id !== 'free' && ( + <> + {' '} + + + current spend + + + )} +
+
+
+
+ Monthly invoice estimate + +
+

Your new monthly invoice

+

+ Each paid project runs on a dedicated 24/7 server. First project uses + Compute Credits; additional ones cost $10+/month regardless of usage.{' '} + + Learn more + + . +

- const computeCreditsItem = - subscriptionPreview?.breakdown?.find((item) => - item.description.startsWith('Compute Credits') - ) ?? null + {subscriptionPreviewError && ( + + )} - const planItem = subscriptionPreview?.breakdown?.find((item) => - item.description?.toLowerCase().includes('plan') - ) + {subscriptionPreviewIsLoading && ( +
+ Estimating monthly costs... + + + +
+ )} - const allProjects = computeItems.flatMap((item) => - item.breakdown.map((project) => ({ - ...project, - computeType: item.description, - computeCosts: Math.round( - item.total_price / item.breakdown.length - ), - })) - ) + {subscriptionPreviewInitialized && ( + <> +
+ + {/* Non-compute items and Projects list */} + {(() => { + // Combine all compute-related projects + const computeItems = + subscriptionPreview?.breakdown?.filter( + (item) => + item.description?.toLowerCase().includes('compute') && + item.breakdown?.length > 0 + ) || [] - const otherItems = - subscriptionPreview?.breakdown?.filter( - (item) => - !item.description?.toLowerCase().includes('compute') && - !item.description?.toLowerCase().includes('plan') - ) || [] + const computeCreditsItem = + subscriptionPreview?.breakdown?.find((item) => + item.description.startsWith('Compute Credits') + ) ?? null - const content = ( - <> - {/* Combined projects section */} - {allProjects.length > 0 && ( - <> - - - {planItem?.description} - - - {formatCurrency(planItem?.total_price)} - - + const planItem = subscriptionPreview?.breakdown?.find( + (item) => item.description?.toLowerCase().includes('plan') + ) - - - Compute - -

- Each project on a paid plan is a dedicated server - running 24/7 with no pausing. The first project is - covered by Compute Credits. Additional projects will - incur compute costs starting at $10/month, - independent of activity. See{' '} - + item.breakdown.map((project) => ({ + ...project, + computeType: item.description, + computeCosts: Math.round( + item.total_price / item.breakdown.length + ), + })) + ) + + const otherItems = + subscriptionPreview?.breakdown?.filter( + (item) => + !item.description?.toLowerCase().includes('compute') && + !item.description?.toLowerCase().includes('plan') + ) || [] + + const content = ( + <> + {/* Combined projects section */} + {allProjects.length > 0 && ( + <> + + + {planItem?.description} + + + {formatCurrency(planItem?.total_price)} + + + + + + Compute + + + {formatCurrency( + computeItems.reduce( + (sum: number, item) => sum + item.total_price, + 0 + ) + (computeCreditsItem?.total_price ?? 0) + )} + + + {/* Show first 3 projects */} + {allProjects.map((project) => ( + - docs - - . -

- {subscription?.plan?.id === 'free' && ( - <> -

- Mixing paid and non-paid projects in a single - organization is not possible. If you want - projects to be on the Free Plan, use self-serve - project transfers. -

-
- - -
- + + {project.project_name} ({project.computeType}) |{' '} + {formatCurrency(project.computeCosts)} + +
+ ))} + {computeCreditsItem && ( + + + Compute Credits |{' '} + {formatCurrency(computeCreditsItem.total_price)} + + )} - - - - {formatCurrency( - computeItems.reduce( - (sum: number, item) => sum + item.total_price, - 0 - ) + (computeCreditsItem?.total_price ?? 0) - )} - - - {/* Show first 3 projects */} - {allProjects.map((project) => ( - - - {project.project_name} ({project.computeType}) |{' '} - {formatCurrency(project.computeCosts)} - - - ))} - {computeCreditsItem && ( - - - Compute Credits |{' '} - {formatCurrency(computeCreditsItem.total_price)} - - + + )} + + {/* Non-compute items */} + {otherItems.map((item) => ( + + +
+ {item.description ?? 'Unknown'} + {item.breakdown.length > 0 && ( + +

Projects using {item.description}:

+
    + {item.breakdown.map((breakdown) => ( +
  • + {breakdown.project_name} +
  • + ))} +
+
+ )} +
+
+ + {formatCurrency(item.total_price)} + +
+ ))} + + ) + return content + })()} + + + + Total per month (excluding other usage) + + + {formatCurrency( + Math.round( + subscriptionPreview?.breakdown?.reduce( + (prev, cur) => prev + cur.total_price, + 0 + ) ?? 0 + ) ?? 0 )} - - )} - - {/* Non-compute items */} - {otherItems.map((item) => ( - - -
- {item.description ?? 'Unknown'} - {item.breakdown.length > 0 && ( - -

Projects using {item.description}:

-
    - {item.breakdown.map((breakdown) => ( -
  • - {breakdown.project_name} -
  • - ))} -
-
- )} -
-
- - {formatCurrency(item.total_price)} - -
- ))} - - ) - return content - })()} - - - - Total per month (excluding other usage) - - - {formatCurrency( - Math.round( - subscriptionPreview?.breakdown?.reduce( - (prev, cur) => prev + cur.total_price, - 0 - ) ?? 0 - ) ?? 0 - )} - - -
-
- - )} - - + + + + + + )} +
+ +
+
+ {formatCurrency( + Math.round( + subscriptionPreview?.breakdown.reduce( + (prev: number, cur) => prev + cur.total_price, + 0 + ) ?? 0 + ) + )} +
+ + )} -
+
{!billingViaPartner ? ( -
+
+ )} -
- @@ -666,5 +586,3 @@ const SubscriptionPlanUpdateDialog = ({
) } - -export default SubscriptionPlanUpdateDialog diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx deleted file mode 100644 index c374abacd19..00000000000 --- a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { zodResolver } from '@hookform/resolvers/zod' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useEffect } from 'react' -import { useForm } from 'react-hook-form' -import { toast } from 'sonner' -import * as z from 'zod' - -import { useParams } from 'common' -import { - ScaffoldSection, - ScaffoldSectionContent, - ScaffoldSectionDetail, -} from 'components/layouts/Scaffold' -import AlertError from 'components/ui/AlertError' -import { FormActions } from 'components/ui/Forms/FormActions' -import NoPermission from 'components/ui/NoPermission' -import Panel from 'components/ui/Panel' -import PartnerManagedResource from 'components/ui/PartnerManagedResource' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useOrganizationTaxIdQuery } from 'data/organizations/organization-tax-id-query' -import { useOrganizationTaxIdUpdateMutation } from 'data/organizations/organization-tax-id-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { - Button, - FormControl_Shadcn_, - FormField_Shadcn_, - FormItem_Shadcn_, - Form_Shadcn_, - Input_Shadcn_, - SelectContent_Shadcn_, - SelectGroup_Shadcn_, - SelectItem_Shadcn_, - SelectTrigger_Shadcn_, - SelectValue_Shadcn_, - Select_Shadcn_, -} from 'ui' -import { TAX_IDS } from './TaxID.constants' -import { checkTaxIdEqual, sanitizeTaxIdValue } from './TaxID.utils' -import { X } from 'lucide-react' - -const TaxID = () => { - const { slug } = useParams() - const selectedOrganization = useSelectedOrganization() - - const { data: taxId, error, isLoading, isSuccess, isError } = useOrganizationTaxIdQuery({ slug }) - const { mutate: updateTaxId, isLoading: isUpdating } = useOrganizationTaxIdUpdateMutation({ - onSuccess: () => { - toast.success('Successfully updated tax id') - }, - }) - - const FormSchema = z.object({ type: z.string(), value: z.string(), name: z.string() }) - const form = useForm>({ - mode: 'onBlur', - reValidateMode: 'onBlur', - resolver: zodResolver(FormSchema), - defaultValues: { type: '', value: '', name: '' }, - }) - - const formattedTaxId = taxId - ? { - type: taxId.type, - value: taxId.value, - name: - TAX_IDS.find( - (option) => option.type === taxId.type && option.countryIso2 === taxId.country - )?.name ?? '', - } - : { type: '', value: '', name: '' } - - const hasChanges = !checkTaxIdEqual(form.getValues(), formattedTaxId) - const canReadTaxId = useCheckPermissions(PermissionAction.BILLING_READ, 'stripe.tax_ids') - const canUpdateTaxId = useCheckPermissions(PermissionAction.BILLING_WRITE, 'stripe.tax_ids') - - useEffect(() => { - if (isSuccess && formattedTaxId) { - form.setValue('type', formattedTaxId.type) - form.setValue('value', formattedTaxId.value) - form.setValue('name', formattedTaxId.name) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccess]) - - const onSaveTaxId = async () => { - if (!slug) return console.error('Slug is required') - - const { type, value } = form.getValues() - - if (type?.length && !value?.length) { - form.setError('value', { message: 'Value is required' }) - return - } - - updateTaxId({ - slug, - taxId: - type?.length && value?.length - ? { - type: type, - value: sanitizeTaxIdValue({ value, name: form.getValues().name }), - country: selectedTaxId?.countryIso2, - } - : null, - }) - } - - const onResetTaxIds = () => { - form.setValue('type', formattedTaxId.type) - form.setValue('value', formattedTaxId.value) - form.setValue('name', formattedTaxId.name) - } - - const onSelectTaxIdType = (name: string) => { - const selectedTaxIdOption = TAX_IDS.find((option) => option.name === name) - if (!selectedTaxIdOption) return - form.setValue('type', selectedTaxIdOption.type) - form.setValue('value', '') - form.setValue('name', name) - } - - const onRemoveTaxId = () => { - form.reset() - } - - const { name } = form.watch() - - const selectedTaxId = TAX_IDS.find((option) => option.name === name) - - return ( - - -
-

Tax ID

-

- Add a tax ID to your invoices. Changes only apply to future invoices. -

-

- Make sure the tax ID looks exactly like the placeholder text. -

-
-
- - {selectedOrganization?.managed_by !== undefined && - selectedOrganization?.managed_by !== 'supabase' ? ( - - ) : !canReadTaxId ? ( - - ) : ( - <> - {isLoading && ( -
- - - -
- )} - - {isError && ( - - )} - - {isSuccess && ( - onResetTaxIds()} - helper={ - !canUpdateTaxId - ? "You need additional permissions to manage this organization's tax ID" - : undefined - } - /> - } - > - - onSaveTaxId())} - > - ( - - - onSelectTaxIdType(value)} - > - - - - - - {TAX_IDS.sort((a, b) => a.country.localeCompare(b.country)).map( - (option) => ( - - {option.country} - {option.name} - - ) - )} - - - - - - )} - /> - - {selectedTaxId && ( -
- ( - - - - - - )} - /> - -
- )} - -
-
- )} - - )} -
-
- ) -} - -export default TaxID diff --git a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSection.tsx b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSection.tsx index 35d8c06199f..673df43cff6 100644 --- a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSection.tsx +++ b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSection.tsx @@ -5,11 +5,14 @@ import { ScaffoldSectionDetail, } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import InvoicesSettings from './InvoicesSettings' const InvoicesSection = () => { - const canReadInvoices = useCheckPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') + const { isSuccess: isPermissionsLoaded, can: canReadInvoices } = useAsyncCheckProjectPermissions( + PermissionAction.BILLING_READ, + 'stripe.subscriptions' + ) return ( @@ -24,7 +27,7 @@ const InvoicesSection = () => { - {!canReadInvoices ? ( + {isPermissionsLoaded && !canReadInvoices ? ( ) : ( diff --git a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx index 4542010b18d..08bb6eeb398 100644 --- a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx +++ b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx @@ -1,6 +1,7 @@ import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js' import type { PaymentMethod } from '@stripe/stripe-js' import { useQueryClient } from '@tanstack/react-query' +import _ from 'lodash' import { Edit2, ExternalLink, HelpCircle } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' @@ -16,6 +17,7 @@ import { invalidateOrganizationsQuery, useOrganizationsQuery, } from 'data/organizations/organizations-query' +import { useProjectsQuery } from 'data/projects/projects-query' import { BASE_PATH, PRICING_TIER_LABELS_ORG } from 'lib/constants' import { getURL } from 'lib/helpers' import { useProfile } from 'lib/profile' @@ -34,9 +36,9 @@ import { TooltipContent, TooltipTrigger, } from 'ui' -import { useProjectsQuery } from 'data/projects/projects-query' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import _ from 'lodash' +import { BillingCustomerDataNewOrgDialog } from '../BillingSettings/BillingCustomerData/BillingCustomerDataNewOrgDialog' +import { FormCustomerData } from '../BillingSettings/BillingCustomerData/useBillingCustomerDataForm' const ORG_KIND_TYPES = { PERSONAL: 'Personal', @@ -101,6 +103,8 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { const [isOrgCreationConfirmationModalVisible, setIsOrgCreationConfirmationModalVisible] = useState(false) + const [customerData, setCustomerData] = useState(null) + const [formState, setFormState] = useState({ plan: 'FREE', name: '', @@ -157,7 +161,8 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { router.push(`/new/${org.slug}?projectName=${prefilledProjectName}`) } }, - onError: () => { + onError: (data) => { + toast.error(`Failed to create organization: ${data.message}`) resetPaymentMethod() setNewOrgLoading(false) }, @@ -182,6 +187,9 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { | 'tier_enterprise', ...(formState.kind == 'COMPANY' ? { size: formState.size } : {}), payment_method: paymentMethodId, + billing_name: dbTier === 'FREE' ? undefined : customerData?.billing_name, + address: dbTier === 'FREE' ? undefined : customerData?.address, + tax_id: dbTier === 'FREE' ? undefined : customerData?.tax_id ?? undefined, }) } @@ -462,6 +470,21 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { )} + {formState.plan !== 'FREE' && ( + +
+
+ + Billing Address + +
+
+ +
+
+
+ )} + {formState.plan !== 'FREE' && ( {paymentMethod?.card !== undefined ? ( diff --git a/apps/studio/components/interfaces/Organization/Usage/Compute.tsx b/apps/studio/components/interfaces/Organization/Usage/Compute.tsx index 50059f297a5..4436a14590d 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Compute.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Compute.tsx @@ -48,6 +48,14 @@ const Compute = ({ orgSlug, projectRef, startDate, endDate }: ComputeProps) => { [ComputeUsageMetric.COMPUTE_HOURS_8XL]: 'red', [ComputeUsageMetric.COMPUTE_HOURS_12XL]: 'dark-red', [ComputeUsageMetric.COMPUTE_HOURS_16XL]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_24XL]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_24XL_OPTIMIZED_CPU]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_24XL_OPTIMIZED_MEMORY]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_24XL_HIGH_MEMORY]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_48XL]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_48XL_OPTIMIZED_CPU]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_48XL_OPTIMIZED_MEMORY]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_48XL_HIGH_MEMORY]: 'purple', } const attributes: Attribute[] = Object.keys(ComputeUsageMetric).map((it) => ({ diff --git a/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx index dd5bb1a399c..cfdcb09cfee 100644 --- a/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx @@ -76,7 +76,7 @@ export const PostgresVersionSelector = ({ const defaultValue = gaVersion ? formatValue(gaVersion) : formatValue(availableVersions[0]) form.setValue('postgresVersionSelection', defaultValue) } - }, [isSuccess, form]) + }, [isSuccess, availableVersions, form]) return ( diff --git a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx index 815081d4923..5c72734c5b4 100644 --- a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx @@ -52,7 +52,9 @@ export const RegionSelector = ({ <>

Select the region closest to your users for the best performance.

{showNonProdFields && ( -

Note: Only SG is supported for local/staging projects

+

+ Note: Only US (NV), Frankfurt and SG are supported for local/staging projects +

)} } diff --git a/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover/index.tsx index 78ff9059186..dccb5840b2f 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover/index.tsx @@ -81,7 +81,7 @@ export const ChooseChannelPopover = ({ config, onChangeConfig }: ChooseChannelPo

- +
{config.channelName.length === 0 ? ( <> diff --git a/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx b/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx index 591f8a63c04..b40b57692af 100644 --- a/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx +++ b/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx @@ -95,7 +95,6 @@ const RenameQueryModal = ({ // [Joshen] For SQL V2 - content is loaded on demand so we need to fetch the data if its not already loaded in the valtio state if (!('content' in localSnippet)) { localSnippet = await getContentById({ projectRef: ref, id }) - snapV2.addSnippet({ projectRef: ref, snippet: localSnippet }) } @@ -109,10 +108,12 @@ const RenameQueryModal = ({ }) snapV2.renameSnippet({ id, name: nameInput, description: descriptionInput }) + if (isSQLEditorTabsEnabled && ref) { const tabId = createTabId('sql', { id }) updateTab(ref, tabId, { label: nameInput }) } + toast.success('Successfully renamed snippet!') if (onComplete) onComplete() } catch (error: any) { diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts index 776575c517c..bb8d77a0cad 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts @@ -22,7 +22,9 @@ export const sqlAiDisclaimerComment = ` export const untitledSnippetTitle = 'Untitled query' -export const destructiveSqlRegex = [/^(.*;)?\s*(drop|delete|truncate)\s/is] +export const destructiveSqlRegex = [ + /^(.*;)?\s*(drop|delete|truncate|alter\s+table\s+.*\s+drop\s+column)\s/is, +] export const ASSISTANT_TEMPLATES = [ { diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx index e518e86627f..d4577ae1a8f 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -53,6 +53,7 @@ import { cn, } from 'ui' import { useSnapshot } from 'valtio' +import { useIsSQLEditorTabsEnabled } from '../App/FeaturePreview/FeaturePreviewContext' import { subscriptionHasHipaaAddon } from '../Billing/Subscription/Subscription.utils' import { useSqlEditorDiff, useSqlEditorPrompt } from './hooks' import { RunQueryWarningModal } from './RunQueryWarningModal' @@ -98,13 +99,13 @@ export const SQLEditor = () => { const isOptedInToAI = useOrgOptedIntoAi() const [selectedSchemas] = useSchemasForAi(project?.ref!) const includeSchemaMetadata = isOptedInToAI || !IS_PLATFORM + const isSQLEditorTabsEnabled = useIsSQLEditorTabsEnabled() const { sourceSqlDiff, setSourceSqlDiff, selectedDiffType, setSelectedDiffType, - pendingTitle, setIsAcceptDiffLoading, isDiffOpen, defaultSqlDiff, @@ -216,6 +217,10 @@ export const SQLEditor = () => { try { const { title: name } = await generateSqlTitle({ sql }) snapV2.renameSnippet({ id, name }) + if (isSQLEditorTabsEnabled && ref) { + const tabId = createTabId('sql', { id }) + updateTab(ref, tabId, { label: name }) + } } catch (error) { // [Joshen] No error handler required as this happens in the background and not necessary to ping the user } @@ -431,10 +436,6 @@ export const SQLEditor = () => { range: editorModel.getFullModelRange(), }, ]) - - if (pendingTitle) { - snapV2.renameSnippet({ id, name: pendingTitle }) - } } sendEvent({ @@ -450,16 +451,7 @@ export const SQLEditor = () => { setIsAcceptDiffLoading(false) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - sourceSqlDiff, - selectedDiffType, - handleNewQuery, - generateSqlTitle, - router, - id, - pendingTitle, - snapV2, - ]) + }, [sourceSqlDiff, selectedDiffType, handleNewQuery, generateSqlTitle, router, id, snapV2]) const discardAiHandler = useCallback(() => { sendEvent({ @@ -670,217 +662,208 @@ export const SQLEditor = () => { }} /> - - - - -
- {isLoading ? ( -
- -
- ) : ( - <> - {isDiffOpen && ( -
- { - diffEditorRef.current = editor - setIsDiffEditorMounted(true) - }} - options={{ - fontSize: 13, - renderSideBySide: false, - minimap: { enabled: false }, - wordWrap: 'on', - lineNumbers: 'on', - folding: false, - padding: { top: 4 }, - lineNumbersMinChars: 3, - }} - /> - {showWidget && ( - { - handlePrompt(prompt, { - beforeSelection: promptState.beforeSelection, - selection: promptState.selection || defaultSqlDiff.modified, - afterSelection: promptState.afterSelection, - }) - }} - onAccept={acceptAiHandler} - onReject={discardAiHandler} - onCancel={resetPrompt} - isDiffVisible={true} - isLoading={isCompletionLoading} - startLineNumber={Math.max(0, promptState.startLineNumber)} - endLineNumber={promptState.endLineNumber} - /> - )} -
- )} -
- { - setPromptState((prev) => ({ - ...prev, - isOpen: true, - selection, - beforeSelection, - afterSelection, - startLineNumber, - endLineNumber, - })) - }} - /> - {editorRef.current && promptState.isOpen && !isDiffOpen && ( - { - handlePrompt(prompt, { - beforeSelection: promptState.beforeSelection, - selection: promptState.selection, - afterSelection: promptState.afterSelection, - }) - }} - onCancel={resetPrompt} - isDiffVisible={false} - isLoading={isCompletionLoading} - startLineNumber={Math.max(0, promptState.startLineNumber)} - endLineNumber={promptState.endLineNumber} - /> - )} - - {!promptState.isOpen && !editorRef.current?.getValue() && ( - - Hit {os === 'macos' ? : `CTRL+`}K to edit with the - Assistant - - )} - -
- - )} -
-
- - - - +
+ + +
{isLoading ? (
) : ( - - )} - - - - {results?.rows !== undefined && !isExecuting && ( - - - -

- - {results.rows.length} row{results.rows.length > 1 ? 's' : ''} - - - {results.autoLimit !== undefined && - ` (Limited to only ${results.autoLimit} rows)`} - -

-
- -

- - Results are automatically limited to preserve browser performance, in - particular if your query returns an exceptionally large number of rows. - - - - You may change or remove this limit from the dropdown on the right - -

-
-
- {results.autoLimit !== undefined && ( - - - - - - snapV2.setLimit(Number(val))} - > - {ROWS_PER_PAGE_OPTIONS.map((option) => ( - - {option.label} - - ))} - - - + <> + {isDiffOpen && ( +
+ { + diffEditorRef.current = editor + setIsDiffEditorMounted(true) + }} + options={{ + fontSize: 13, + renderSideBySide: false, + minimap: { enabled: false }, + wordWrap: 'on', + lineNumbers: 'on', + folding: false, + padding: { top: 4 }, + lineNumbersMinChars: 3, + }} + /> + {showWidget && ( + { + handlePrompt(prompt, { + beforeSelection: promptState.beforeSelection, + selection: promptState.selection || defaultSqlDiff.modified, + afterSelection: promptState.afterSelection, + }) + }} + onAccept={acceptAiHandler} + onReject={discardAiHandler} + onCancel={resetPrompt} + isDiffVisible={true} + isLoading={isCompletionLoading} + startLineNumber={Math.max(0, promptState.startLineNumber)} + endLineNumber={promptState.endLineNumber} + /> + )} +
)} -
+
+ { + setPromptState((prev) => ({ + ...prev, + isOpen: true, + selection, + beforeSelection, + afterSelection, + startLineNumber, + endLineNumber, + })) + }} + /> + {editorRef.current && promptState.isOpen && !isDiffOpen && ( + { + handlePrompt(prompt, { + beforeSelection: promptState.beforeSelection, + selection: promptState.selection, + afterSelection: promptState.afterSelection, + }) + }} + onCancel={resetPrompt} + isDiffVisible={false} + isLoading={isCompletionLoading} + startLineNumber={Math.max(0, promptState.startLineNumber)} + endLineNumber={promptState.endLineNumber} + /> + )} + + {!promptState.isOpen && !editorRef.current?.getValue() && ( + + Hit {os === 'macos' ? : `CTRL+`}K to edit with the + Assistant + + )} + +
+ )} -
- - - +
+
+ + + + + {isLoading ? ( +
+ +
+ ) : ( + + )} +
+ +
+ {results?.rows !== undefined && !isExecuting && ( + + + +

+ + {results.rows.length} row{results.rows.length > 1 ? 's' : ''} + + + {results.autoLimit !== undefined && + ` (Limited to only ${results.autoLimit} rows)`} + +

+
+ +

+ + Results are automatically limited to preserve browser performance, in + particular if your query returns an exceptionally large number of rows. + + + + You may change or remove this limit from the dropdown on the right + +

+
+
+ {results.autoLimit !== undefined && ( + + + + + + snapV2.setLimit(Number(val))} + > + {ROWS_PER_PAGE_OPTIONS.map((option) => ( + + {option.label} + + ))} + + + + )} +
+ )} +
+
+
) } diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts index bea8f7a8f91..b8736ded8e1 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts @@ -246,4 +246,17 @@ describe('SQLEditor.utils:updateWithoutWhere', () => { expect(match).toBe(true) }) + + it('should catch potential destructive queries', () => { + const DESTRUCTIVE_QUERIES = [ + `ALTER TABLE test DROP COLUMN test;`, + `DELETE FROM test;`, + `DROP TABLE test;`, + `TRUNCATE TABLE test;`, + ] + + DESTRUCTIVE_QUERIES.forEach((query) => { + expect(checkDestructiveQuery(query), `Query ${query} should be destructive`).toBe(true) + }) + }) }) diff --git a/apps/studio/components/interfaces/SQLEditor/hooks.ts b/apps/studio/components/interfaces/SQLEditor/hooks.ts index 29d8d0cc824..8c5550ca94a 100644 --- a/apps/studio/components/interfaces/SQLEditor/hooks.ts +++ b/apps/studio/components/interfaces/SQLEditor/hooks.ts @@ -67,7 +67,6 @@ export const useNewQuery = () => { export function useSqlEditorDiff() { const [sourceSqlDiff, setSourceSqlDiff] = useState() const [selectedDiffType, setSelectedDiffType] = useState() - const [pendingTitle, setPendingTitle] = useState() const [isAcceptDiffLoading, setIsAcceptDiffLoading] = useState(false) const isDiffOpen = !!sourceSqlDiff @@ -91,7 +90,6 @@ export function useSqlEditorDiff() { const closeDiff = useCallback(() => { setSourceSqlDiff(undefined) - setPendingTitle(undefined) setSelectedDiffType(undefined) }, []) @@ -100,8 +98,6 @@ export function useSqlEditorDiff() { setSourceSqlDiff, selectedDiffType, setSelectedDiffType, - pendingTitle, - setPendingTitle, isAcceptDiffLoading, setIsAcceptDiffLoading, isDiffOpen, diff --git a/apps/studio/components/interfaces/Settings/API/JWTSettings.tsx b/apps/studio/components/interfaces/Settings/API/JWTSettings.tsx index deafd573c5e..0e944240265 100644 --- a/apps/studio/components/interfaces/Settings/API/JWTSettings.tsx +++ b/apps/studio/components/interfaces/Settings/API/JWTSettings.tsx @@ -9,10 +9,14 @@ import { ChevronDown, Eye, EyeOff, + Hourglass, Key, + KeyRound, Loader2, PenTool, + Power, RefreshCw, + Undo, } from 'lucide-react' import { Dispatch, SetStateAction, useState } from 'react' import { toast } from 'sonner' @@ -20,12 +24,13 @@ import { number, object } from 'yup' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { FormActions } from 'components/ui/Forms/FormActions' import Panel from 'components/ui/Panel' +import { useAuthConfigQuery } from 'data/auth/auth-config-query' +import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useJwtSecretUpdateMutation } from 'data/config/jwt-secret-update-mutation' import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' -import { useAuthConfigQuery } from 'data/auth/auth-config-query' -import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { uuidv4 } from 'lib/helpers' import { @@ -38,21 +43,18 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, + Form, Input, InputNumber, Modal, WarningIcon, - Form, } from 'ui' +import { Admonition } from 'ui-patterns' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { JWT_SECRET_UPDATE_ERROR_MESSAGES, JWT_SECRET_UPDATE_PROGRESS_MESSAGES, } from './API.constants' -import { Admonition } from 'ui-patterns' -import { FormActions } from 'components/ui/Forms/FormActions' -import { FormPanel } from 'components/ui/Forms/FormPanel' -import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' const schema = object({ JWT_EXP: number() @@ -133,7 +135,10 @@ const JWTSettings = () => { toast( 'Successfully submitted JWT secret update request. Please wait while your project is updated.' ) - } catch (error) {} + } catch (error) { + toast.error('Failed to update JWT secret. Please try again.') + console.error('Failed to update JWT secret:', error) + } } return ( @@ -296,30 +301,67 @@ const JWTSettings = () => { setIsGeneratingKey(false)} onConfirm={() => handleJwtSecretUpdate('ROLL', setIsGeneratingKey)} - alert={{ - title: 'This will invalidate all existing API keys', - description: ( - <> - Generating a new JWT secret will invalidate all of - your API keys, including your service_role and{' '} - anon keys. Your project will also be restarted during - this process, which will terminate any existing connections. You may receive API - errors for up to 2 minutes while the new secret is deployed. - - ), - }} > -

- This action cannot be undone and the old JWT secret will be lost. All existing API keys - will be invalidated, and any open connections will be terminated. -

+
    +
  • + + + +
    +

    This will invalidate all API keys

    +

    + Generating a new JWT secret will invalidate all{' '} + existing keys, including service_role{' '} + + secret + {' '} + and anon{' '} + public. +

    +
    +
  • + +
  • + + + +
    +

    Your project will be restarted

    +

    + This process restarts your project, terminating existing connections. You may see + API errors for up to 2 minutes while the new secret is deployed. +

    +
    +
  • +
  • + + + +
    +

    20-minute cooldown

    +

    Keys can only be regenerated every 20 minutes.

    +
    +
  • +
  • + + + +
    +

    This cannot be undone

    +

    + The old JWT secret will be lost, all API keys invalidated, and open connections + terminated. +

    +
    +
  • +
{ const isBranch = parentProject !== undefined const isProjectActive = useIsProjectActive() const isOrioleDb = useIsOrioleDb() + const isOrioleDbInAws = useIsOrioleDbInAws() const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: selectedOrg?.slug }) @@ -486,7 +487,7 @@ const Addons = () => { - ) : isOrioleDb ? ( + ) : isOrioleDbInAws ? ( { type="default" onClick={onCancelCustomDomain} loading={isDeleting} - disabled={isValidating} className="self-end" > Cancel diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx index cdb19a8e7d7..48f2d896168 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx @@ -191,7 +191,9 @@ const DeployNewReplicaPanel = ({ const availableRegions = process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging' - ? AVAILABLE_REPLICA_REGIONS.filter((x) => x.key === 'SOUTHEAST_ASIA') + ? AVAILABLE_REPLICA_REGIONS.filter((x) => + ['SOUTHEAST_ASIA', 'CENTRAL_EU', 'EAST_US'].includes(x.key) + ) : AVAILABLE_REPLICA_REGIONS const onSubmit = async () => { diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx index 2a9671e7b2a..db1822501ed 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx @@ -13,6 +13,7 @@ import { useProjectUpgradeEligibilityQuery } from 'data/config/project-upgrade-e import { useProjectServiceVersionsQuery } from 'data/projects/project-service-versions' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' +import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, @@ -62,7 +63,7 @@ const InfrastructureInfo = () => { current_app_version_release_channel && current_app_version_release_channel !== 'ga' ? current_app_version_release_channel : undefined - const isOrioleDb = (current_app_version ?? '').includes('orioledb') + const isOrioleDb = useIsOrioleDb() const latestPgVersion = (latest_app_version ?? '').split('supabase-postgres-')[1] const isInactive = project?.status === 'INACTIVE' @@ -147,18 +148,16 @@ const InfrastructureInfo = () => { ), isOrioleDb && ( - <> - - - - OrioleDB - - - - This project uses OrioleDB - - - + + + + OrioleDB + + + + This project uses OrioleDB + + ), isOnLatestVersion && ( diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx b/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx index 942dd9cb619..a113cd1539f 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx @@ -1,16 +1,16 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import DatePicker from 'react-datepicker' import dayjs from 'dayjs' -import { useEffect, useState } from 'react' - -import { Button, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, cn } from 'ui' -import type { DatetimeHelper } from './Logs.types' import { ChevronLeft, ChevronRight, Clock, XIcon } from 'lucide-react' -import TimeSplitInput from 'components/ui/DatePicker/TimeSplitInput' -import { RadioGroup, RadioGroupItem } from '@ui/components/shadcn/ui/radio-group' +import { PropsWithChildren, useEffect, useState } from 'react' +import DatePicker from 'react-datepicker' + import { Label } from '@ui/components/shadcn/ui/label' +import { RadioGroup, RadioGroupItem } from '@ui/components/shadcn/ui/radio-group' +import TimeSplitInput from 'components/ui/DatePicker/TimeSplitInput' +import { Button, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, cn } from 'ui' import { LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD } from './Logs.constants' -import { PropsWithChildren } from 'react' +import type { DatetimeHelper } from './Logs.types' +import { copyToClipboard } from 'lib/helpers' export type DatePickerValue = { to: string @@ -176,12 +176,13 @@ export const LogsDatePicker = ({ onSubmit, helpers, value }: PropsWithChildren

{ - document.addEventListener('paste', handlePaste) - document.addEventListener('copy', handleCopy) + if (open) { + document.addEventListener('paste', handlePaste) + document.addEventListener('copy', handleCopy) + } return () => { document.removeEventListener('paste', handlePaste) document.removeEventListener('copy', handleCopy) } - }, [startDate, endDate]) + }, [open, startDate, endDate]) const isLargeRange = Math.abs(dayjs(startDate).diff(dayjs(endDate), 'days')) > @@ -215,7 +218,12 @@ export const LogsDatePicker = ({ onSubmit, helpers, value }: PropsWithChildren

- + {options.label} - +

{ e.preventDefault() diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx index 76f8b95560c..cdd3a78d9ce 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx @@ -28,6 +28,8 @@ import { import type { Filters, LogSearchCallback, LogTemplate, QueryType } from './Logs.types' import { maybeShowUpgradePrompt } from './Logs.utils' import UpgradePrompt from './UpgradePrompt' +import { useFlag } from 'hooks/ui/useFlag' +import { PreviewFilterPanelWithUniversal } from './PreviewFilterPanelWithUniversal' /** * Acts as a container component for the entire log display @@ -58,6 +60,8 @@ export const LogsPreviewer = ({ EmptyState, filterPanelClassName, }: PropsWithChildren) => { + const useUniversalFilterBar = useFlag('universalFilterBar') + const router = useRouter() const { db } = useParams() const organization = useSelectedOrganization() @@ -186,39 +190,48 @@ export const LogsPreviewer = ({ } }, [db, isSuccess]) + // Common props shared between both filter panel components to avoid duplication + const filterPanelProps = { + className: filterPanelClassName, + csvData: logData, + isLoading, + newCount, + onRefresh: handleRefresh, + onSearch: handleSearch, + defaultSearchValue: search, + defaultToValue: timestampEnd, + defaultFromValue: timestampStart, + queryUrl: `/project/${projectRef}/logs/explorer?q=${encodeURIComponent( + params.sql || '' + )}&its=${encodeURIComponent(timestampStart)}&ite=${encodeURIComponent(timestampEnd)}`, + onSelectTemplate, + filters, + onFiltersChange: setFilters, + table, + condensedLayout, + isShowingEventChart: showChart, + onToggleEventChart: () => setShowChart(!showChart), + onSelectedDatabaseChange: (id: string) => { + setFilters({ ...filters, database: id !== projectRef ? id : undefined }) + const { db, ...params } = router.query + router.push({ + pathname: router.pathname, + query: id !== projectRef ? { ...router.query, db: id } : params, + }) + }, + selectedDatePickerValue, + setSelectedDatePickerValue, + } + return (
- setShowChart(!showChart)} - onSelectedDatabaseChange={(id: string) => { - setFilters({ ...filters, database: id !== projectRef ? id : undefined }) - const { db, ...params } = router.query - router.push({ - pathname: router.pathname, - query: id !== projectRef ? { ...router.query, db: id } : params, - }) - }} - selectedDatePickerValue={selectedDatePickerValue} - setSelectedDatePickerValue={setSelectedDatePickerValue} - /> + {useUniversalFilterBar ? ( + // Experimental Universal Filter Bar + + ) : ( + // Legacy Filter Panel + + )} {children}
void +} + +function CustomDateRangePicker({ onChange, onCancel }: CustomOptionProps) { + const [dateRange, setDateRange] = useState() + + return ( +
+ +
+ + +
+
+ ) +} + +interface PreviewFilterPanelProps { + defaultSearchValue?: string + defaultToValue?: string + defaultFromValue?: string + templates?: any + isLoading: boolean + newCount: number + onRefresh?: () => void + onSearch?: LogSearchCallback + onExploreClick?: () => void + queryUrl: string + onSelectTemplate: (template: LogTemplate) => void + table: LogsTableName + condensedLayout: Boolean + isShowingEventChart: boolean + onToggleEventChart: () => void + csvData?: unknown[] + onFiltersChange: (filters: Filters) => void + filters: Filters + onSelectedDatabaseChange: (id: string) => void + className?: string + selectedDatePickerValue: DatePickerValue + setSelectedDatePickerValue: (value: DatePickerValue) => void +} + +function useDebounce void>(callback: T, delay: number) { + const timeoutRef = useRef() + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + } + }, []) + + return useCallback( + (...args: Parameters) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + + timeoutRef.current = setTimeout(() => { + callback(...args) + }, delay) + }, + [callback, delay] + ) as T +} + +const PreviewFilterPanelWithUniversal = ({ + isLoading, + newCount, + onRefresh, + onSearch = () => {}, + defaultSearchValue = '', + defaultFromValue, + defaultToValue, + onExploreClick, + queryUrl, + condensedLayout, + isShowingEventChart, + onToggleEventChart, + csvData, + onFiltersChange, + filters, + table, + onSelectedDatabaseChange, + className, + selectedDatePickerValue, + setSelectedDatePickerValue, +}: PreviewFilterPanelProps) => { + const router = useRouter() + const { ref } = useParams() + + const logName = router.pathname.split('/').pop() + + const { data: loadBalancers } = useLoadBalancersQuery({ projectRef: ref }) + + const showDatabaseSelector = + IS_PLATFORM && LOG_ROUTES_WITH_REPLICA_SUPPORT.includes(router.pathname) + + const filterProperties = useMemo(() => { + const tableFilters = FILTER_OPTIONS[table] + if (!tableFilters) return [] + + const properties: FilterProperty[] = [] + + // Add date range filter + properties.push({ + label: 'Time Range', + name: 'timerange', + type: 'string' as const, + operators: ['='], + options: [ + { + label: 'Last 15 minutes', + value: `${dayjs().subtract(15, 'minute').toISOString()}|${dayjs().toISOString()}`, + }, + { + label: 'Last 30 minutes', + value: `${dayjs().subtract(30, 'minute').toISOString()}|${dayjs().toISOString()}`, + }, + { + label: 'Last hour', + value: `${dayjs().subtract(1, 'hour').toISOString()}|${dayjs().toISOString()}`, + }, + { + label: 'Last 3 hours', + value: `${dayjs().subtract(3, 'hour').toISOString()}|${dayjs().toISOString()}`, + }, + { + label: 'Last 24 hours', + value: `${dayjs().subtract(24, 'hour').toISOString()}|${dayjs().toISOString()}`, + }, + { + label: 'Custom Range...', + component: (props: CustomOptionProps) => , + }, + ], + }) + + // Add table-specific filters + Object.entries(tableFilters).forEach(([key, filterSet]) => { + properties.push({ + label: filterSet.label, + name: filterSet.key, + type: 'string' as const, + operators: ['='], + options: filterSet.options.map((option) => ({ + label: option.label, + value: option.key, + description: option.description, + })), + }) + }) + + return properties + }, [table]) + + // Convert Logs.Filters to FilterBar.FilterGroup + const filterBarFilters = useMemo(() => { + const conditions: FilterCondition[] = [] + + // Handle date range - only add from defaultValues if no timerange filter exists + if ((defaultFromValue || defaultToValue) && !filters.timerange) { + conditions.push({ + propertyName: 'timerange', + operator: '=', + value: `${defaultFromValue || ''}|${defaultToValue || ''}`, + }) + } + + // Convert other filters + Object.entries(filters).forEach(([key, value]) => { + if (key === 'search_query') return // Skip search_query as it's handled in freeform text + + if (typeof value === 'object' && value !== null) { + // If the filter object is empty, add a condition with no value + if (Object.keys(value).length === 0) { + conditions.push({ + propertyName: key, + operator: '=', + value: '', + }) + return + } + + // Handle active filters + Object.entries(value as Record).forEach(([subKey, isEnabled]) => { + if (isEnabled) { + conditions.push({ + propertyName: key, + operator: '=', + value: subKey, + }) + } + }) + } else if (value !== undefined && value !== null) { + conditions.push({ + propertyName: key, + operator: '=', + value: String(value), + }) + } + }) + + return { + logicalOperator: 'AND', + conditions, + } as FilterGroup + }, [filters, defaultFromValue, defaultToValue]) + + const handleFilterChange = (filterGroup: FilterGroup) => { + const newFilters: Filters = {} + let hasTimeRange = false + + filterGroup.conditions.forEach((condition) => { + if (!('propertyName' in condition)) return + + const propertyName = condition.propertyName + if (!propertyName) return + + if (propertyName === 'timerange') { + hasTimeRange = true + if (condition.value) { + const value = String(condition.value) + const [from, to] = value.split('|') + newFilters.timerange = { [value]: true } + onSearch('datepicker-change', { from, to }) + } else { + newFilters.timerange = {} + onSearch('datepicker-change', { from: '', to: '' }) + } + } else { + if (!newFilters[propertyName]) { + newFilters[propertyName] = {} + } + + if (!condition.value) { + return + } + + ;(newFilters[propertyName] as Record)[condition.value] = true + } + }) + + // If timerange was completely removed from the filter group + if (!hasTimeRange) { + onSearch('datepicker-change', { from: '', to: '' }) + } + + onFiltersChange(newFilters) + } + + return ( +
+
+
+ onSearch('search-input-change', { query: value })} + /> +
+ +
+
+ } + loading={isLoading} + disabled={isLoading} + onClick={onRefresh} + /> + + + + {/* */} + + {showDatabaseSelector ? ( + <> + + + + + + Open query in Logs Explorer + + + 0 + ? [{ id: `${ref}-all`, name: 'API Load Balancer' }] + : [] + : [] + } + /> + + ) : ( + + )} +
+
+
+ ) +} + +export { PreviewFilterPanelWithUniversal } diff --git a/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx b/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx index 3d45396a016..361f79ea9fe 100644 --- a/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx @@ -2,7 +2,6 @@ import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { toast } from 'sonner' -import { useParams } from 'common' import { useTableFilter } from 'components/grid/hooks/useTableFilter' import type { SupaRow } from 'components/grid/types' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -12,10 +11,6 @@ import { useTableRowDeleteAllMutation } from 'data/table-rows/table-row-delete-a import { useTableRowDeleteMutation } from 'data/table-rows/table-row-delete-mutation' import { useTableRowTruncateMutation } from 'data/table-rows/table-row-truncate-mutation' import { useTableDeleteMutation } from 'data/tables/table-delete-mutation' -import { TablesData, useGetTables } from 'data/tables/tables-query' -import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' -import { useRouter } from 'next/router' -import { useCallback } from 'react' import { useGetImpersonatedRoleState } from 'state/role-impersonation-state' import { useTableEditorStateSnapshot } from 'state/table-editor' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, Checkbox } from 'ui' @@ -23,23 +18,17 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' export type DeleteConfirmationDialogsProps = { selectedTable?: TableLike + onTableDeleted?: () => void } -const DeleteConfirmationDialogs = ({ selectedTable }: DeleteConfirmationDialogsProps) => { - const { ref: projectRef } = useParams() - const router = useRouter() - +const DeleteConfirmationDialogs = ({ + selectedTable, + onTableDeleted, +}: DeleteConfirmationDialogsProps) => { const { project } = useProjectContext() const snap = useTableEditorStateSnapshot() - const { selectedSchema } = useQuerySchemaState() - const { filters, onApplyFilters } = useTableFilter() - const getTables = useGetTables({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - const removeDeletedColumnFromFiltersAndSorts = ({ columnName, }: { @@ -69,9 +58,8 @@ const DeleteConfirmationDialogs = ({ selectedTable }: DeleteConfirmationDialogsP }) const { mutate: deleteTable } = useTableDeleteMutation({ onSuccess: async () => { - const tables = await getTables(selectedSchema) - onAfterDeleteTable(tables) toast.success(`Successfully deleted table "${selectedTable?.name}"`) + onTableDeleted?.() }, onError: (error) => { toast.error(`Failed to delete ${selectedTable?.name}: ${error.message}`) @@ -208,18 +196,6 @@ const DeleteConfirmationDialogs = ({ selectedTable }: DeleteConfirmationDialogsP } } - const onAfterDeleteTable = useCallback( - (tables: TablesData) => { - // For simplicity for now, we just open the first table within the same schema - if (tables.length > 0) { - router.push(`/project/${projectRef}/editor/${tables[0].id}`) - } else { - router.push(`/project/${projectRef}/editor`) - } - }, - [router, projectRef] - ) - return ( <> x.endsWith(id)) : undefined + const getTables = useGetTables({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const onClearDashboardHistory = () => { + if (projectRef && editor) { + appSnap.setDashboardHistory(projectRef, editor === 'table' ? 'editor' : editor, undefined) + } + } + const onTableCreated = useCallback( (table: { id: number }) => { router.push(`/project/${projectRef}/editor/${table.id}`) @@ -63,11 +78,21 @@ export const TableGridEditor = ({ [projectRef, router] ) - const onClearDashboardHistory = () => { - if (projectRef && editor) { - appSnap.setDashboardHistory(projectRef, editor === 'table' ? 'editor' : editor, undefined) + const onTableDeleted = useCallback(async () => { + // For simplicity for now, we just open the first table within the same schema + if (isTableEditorTabsEnabled && selectedTable) { + // Close tab + const tabId = createTabId(selectedTable.entity_type, { id: selectedTable.id }) + handleTabClose({ ref: projectRef, id: tabId, router, editor, onClearDashboardHistory }) + } else { + const tables = await getTables(selectedSchema) + if (tables.length > 0) { + router.push(`/project/${projectRef}/editor/${tables[0].id}`) + } else { + router.push(`/project/${projectRef}/editor`) + } } - } + }, [getTables, isTableEditorTabsEnabled, projectRef, router, selectedSchema]) // NOTE: DO NOT PUT HOOKS AFTER THIS LINE if (isLoadingSelectedTable || !projectRef) { @@ -164,6 +189,7 @@ export const TableGridEditor = ({ />
diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LocalVersionPopover.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LocalVersionPopover.tsx index 1fdd245e201..6adaa0abf2b 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LocalVersionPopover.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LocalVersionPopover.tsx @@ -28,15 +28,18 @@ import { getSemver, semverGte, semverLte } from './LocalVersionPopover.utils' export const LocalVersionPopover = () => { const { data, isSuccess } = useCLIReleaseVersionQuery() const currentCliVersion = data?.current - const hasLatestCLIVersion = isSuccess && !!data?.latest + const latestCliVersion = data?.latest + const hasLatestCLIVersion = isSuccess && !!latestCliVersion - const current = getSemver(data?.current) - const latest = getSemver(data?.latest) + const current = getSemver(currentCliVersion) + const latest = getSemver(latestCliVersion) const hasUpdate = - !!current && !!latest ? data?.current !== data?.latest && semverLte(current, latest) : false + !!current && !!latest + ? currentCliVersion !== latestCliVersion && semverLte(current, latest) + : false const isBeta = - !!current && !!latest && data?.current !== data?.latest && semverGte(current, latest) + !!current && !!latest && currentCliVersion !== latestCliVersion && semverGte(current, latest) const approximateNextRelease = !!data?.published_at ? dayjs(data?.published_at).utc().add(14, 'day').format('DD MMM YYYY') @@ -180,7 +183,7 @@ export const LocalVersionPopover = () => { {hasLatestCLIVersion && hasUpdate && !isBeta && (

Available version:

-

{data.latest}

+

{latestCliVersion}

)}
diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx index 819a3668a70..bf3a851352d 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx @@ -5,6 +5,7 @@ import { PROJECT_STATUS } from 'lib/constants' import { AiAssistantStateContextProvider } from 'state/ai-assistant-state' import { DatabaseSelectorStateContextProvider } from 'state/database-selector' import { RoleImpersonationStateContextProvider } from 'state/role-impersonation-state' +import { StorageExplorerStateContextProvider } from 'state/storage-explorer' import { TableEditorStateContextProvider } from 'state/table-editor' export interface ProjectContextType { @@ -45,11 +46,13 @@ export const ProjectContextProvider = ({ key={`ai-assistant-state-${projectRef}`} projectRef={projectRef} > - - - {children} - - + + + + {children} + + + diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index acd0d43da17..da348ff170e 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -208,7 +208,7 @@ const ProjectLayout = forwardRef
{showPausedState ? ( diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx index 20b432d04ad..cd9795b089a 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx @@ -25,7 +25,7 @@ export function useProjectSettingsGotoCommands(options?: CommandOptions) { { id: 'nav-project-settings-auth', name: 'Auth Settings', - route: `/project/${ref}/settings/auth`, + route: `/project/${ref}/auth/providers`, defaultHidden: true, }, { diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx index 52b458484e9..15b3be04104 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx @@ -1,15 +1,16 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useDebounce } from '@uidotdev/usehooks' +import { FilePlus, FolderPlus, Plus, X } from 'lucide-react' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { toast } from 'sonner' + import { useParams } from 'common' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useLocalStorage } from 'hooks/misc/useLocalStorage' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { LOCAL_STORAGE_KEYS } from 'lib/constants' import { useProfile } from 'lib/profile' -import { FilePlus, FolderPlus, Plus, X } from 'lucide-react' -import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' -import { toast } from 'sonner' import { getAppStateSnapshot } from 'state/app-state' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { @@ -37,8 +38,8 @@ export const SQLEditorMenu = () => { const { profile } = useProfile() const project = useSelectedProject() const { ref } = useParams() - const snapV2 = useSqlEditorV2StateSnapshot() + const [search, setSearch] = useState('') const [showSearch, setShowSearch] = useState(false) const [sort, setSort] = useLocalStorage<'name' | 'inserted_at'>( diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx index 31a02e6c9fe..cb26ba3cd0c 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx @@ -29,7 +29,7 @@ import { useSnippetFolders, useSqlEditorV2StateSnapshot, } from 'state/sql-editor-v2' -import { createTabId, getTabsStore, makeTabPermanent, removeTabs } from 'state/tabs' +import { createTabId, getTabsStore, makeTabPermanent, removeTabs, useTabsStore } from 'state/tabs' import { SqlSnippets } from 'types' import { Separator, TreeView } from 'ui' import { @@ -40,7 +40,6 @@ import { InnerSideMenuSeparator, } from 'ui-patterns' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import { useSnapshot } from 'valtio' import SQLEditorLoadingSnippets from './SQLEditorLoadingSnippets' import { formatFolderResponseForTreeView, getLastItemIds, ROOT_NODE } from './SQLEditorNav.utils' import { SQLEditorTreeViewItem } from './SQLEditorTreeViewItem' @@ -61,7 +60,7 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => { const snapV2 = useSqlEditorV2StateSnapshot() const tabStore = getTabsStore(projectRef) - const tabs = useSnapshot(tabStore) + const tabs = useTabsStore(projectRef) const isSQLEditorTabsEnabled = useIsSQLEditorTabsEnabled() const [sectionVisibility, setSectionVisibility] = useLocalStorage( @@ -540,11 +539,10 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => { }, [projectRef, sharedSqlSnippetsData?.pages]) useEffect(() => { - if (projectRef && isSuccess && isSharedSqlSnippetsSuccess && isSQLEditorTabsEnabled) { + if (projectRef && isSuccess && isSQLEditorTabsEnabled) { sqlEditorTabsCleanup({ ref: projectRef, snippets: allSnippetsInView as any }) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccess, isSharedSqlSnippetsSuccess, allSnippetsInView]) + }, [isSuccess, isSharedSqlSnippetsSuccess, allSnippetsInView, isSQLEditorTabsEnabled]) return ( <> diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SearchList.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SearchList.tsx index 3a7305a4bcc..9c74944dbc9 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SearchList.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SearchList.tsx @@ -29,7 +29,7 @@ export const SearchList = ({ search }: SearchListProps) => { { keepPreviousData: true } ) - const { data: count, isLoading: isLoadingCount } = useContentCountQuery( + const { data: count } = useContentCountQuery( { projectRef, cumulative: true, diff --git a/apps/studio/components/layouts/StorageLayout/StorageMenu.tsx b/apps/studio/components/layouts/StorageLayout/StorageMenu.tsx index afd788fd3af..a91a4f3de24 100644 --- a/apps/studio/components/layouts/StorageLayout/StorageMenu.tsx +++ b/apps/studio/components/layouts/StorageLayout/StorageMenu.tsx @@ -51,8 +51,13 @@ const StorageMenu = () => { | 'usage' | 'logs' - const { data, error, isLoading, isError, isSuccess } = useBucketsQuery({ projectRef: ref }) - const buckets = data ?? [] + const { + data: buckets = [], + error, + isLoading, + isError, + isSuccess, + } = useBucketsQuery({ projectRef: ref }) const sortedBuckets = sort === 'alphabetical' ? buckets.sort((a, b) => diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index 1474176cd1a..3c4ea8c51bb 100644 --- a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -40,6 +40,7 @@ import { InnerSideBarFilters, } from 'ui-patterns/InnerSideMenu' import { useProjectContext } from '../ProjectLayout/ProjectContext' +import { tableEditorTabsCleanUp } from '../Tabs/Tabs.utils' import EntityListItem from './EntityListItem' import { TableMenuEmptyState } from './TableMenuEmptyState' @@ -48,8 +49,8 @@ const TableEditorMenu = () => { const id = _id ? Number(_id) : undefined const snap = useTableEditorStateSnapshot() const { selectedSchema, setSelectedSchema } = useQuerySchemaState() - const isMobile = useBreakpoint() const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled() + const isMobile = useBreakpoint() const [showModal, setShowModal] = useState(false) const [searchText, setSearchText] = useState('') @@ -114,6 +115,13 @@ const TableEditorMenu = () => { } }, [selectedTable?.schema]) + useEffect(() => { + // Clean up tabs + recent items for any tables that might have been removed outside of the dashboard session + if (isTableEditorTabsEnabled && ref && entityTypes && !searchText) { + tableEditorTabsCleanUp({ ref, schemas: [selectedSchema], entities: entityTypes }) + } + }, [entityTypes]) + return ( <>
diff --git a/apps/studio/components/layouts/Tabs/Tabs.utils.ts b/apps/studio/components/layouts/Tabs/Tabs.utils.ts index e9636ab5177..9fc1e1225c3 100644 --- a/apps/studio/components/layouts/Tabs/Tabs.utils.ts +++ b/apps/studio/components/layouts/Tabs/Tabs.utils.ts @@ -5,7 +5,7 @@ import { RecentItem, removeRecentItems, } from 'state/recent-items' -import { createTabId, getTabsStore, removeTabs } from 'state/tabs' +import { createTabId, editorEntityTypes, getTabsStore, removeTabs, updateTab } from 'state/tabs' export const tableEditorTabsCleanUp = ({ ref, @@ -58,6 +58,18 @@ export const tableEditorTabsCleanUp = ({ // perform tabs cleanup removeTabs(ref, tableEditorTabsToBeCleaned) + + // [Joshen] Validate for opened tabs, if their label matches the entity's name - update label if not + // As the entity could've been renamed outside of the table editor + // e.g Using the SQL editor to rename the entity + const openTabs = tabsStore.openTabs + .map((id) => tabsStore.tabsMap[id]) + .filter((tab) => editorEntityTypes['table']?.includes(tab.type)) + + openTabs.forEach((tab) => { + const entity = entities?.find((x) => tab.metadata?.tableId === x.id) + if (!!entity && entity.name !== tab.label) updateTab(ref, tab.id, { label: entity.name }) + }) } export const sqlEditorTabsCleanup = ({ @@ -65,7 +77,7 @@ export const sqlEditorTabsCleanup = ({ snippets, }: { ref: string - snippets: { id: string; type: string }[] + snippets: { id: string; type: string; name: string }[] }) => { // these are tabs that are static content // these canot be removed from localstorage based on this query request @@ -95,4 +107,16 @@ export const sqlEditorTabsCleanup = ({ ? recentItems.filter((item) => !currentContentIds.includes(item.id)).map((item) => item.id) : [] ) + + // [Joshen] Validate for opened tabs, if their label matches the snippet's name - update label if not + // As the snippets name could've been updated outside of the SQL Editor session + // e.g for a shared snippet, the owner could've updated the name of the snippet + const openSqlTabs = tabsStore.openTabs + .map((id) => tabsStore.tabsMap[id]) + .filter((tab) => editorEntityTypes['sql']?.includes(tab.type)) + + openSqlTabs.forEach((tab) => { + const snippet = snippets?.find((x) => tab.metadata?.sqlId === x.id) + if (!!snippet && snippet.name !== tab.label) updateTab(ref, tab.id, { label: snippet.name }) + }) } diff --git a/apps/studio/components/to-be-cleaned/Storage/Storage.types.ts b/apps/studio/components/to-be-cleaned/Storage/Storage.types.ts index e52f7011b57..b29ce672669 100644 --- a/apps/studio/components/to-be-cleaned/Storage/Storage.types.ts +++ b/apps/studio/components/to-be-cleaned/Storage/Storage.types.ts @@ -15,10 +15,12 @@ export interface StorageItem { type: STORAGE_ROW_TYPES status: STORAGE_ROW_STATUS metadata: StorageItemMetadata | null - isCorrupted: boolean created_at: string | null updated_at: string | null last_accessed_at: string | null + // UI specific properties, not from API + isCorrupted: boolean + path?: string } export type StorageItemWithColumn = StorageItem & { columnIndex: number } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx index 3cfb338bee8..3fa51bfcdfd 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx @@ -6,6 +6,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' import { ChevronRight, ChevronsDown, ChevronsUp, Clipboard, Eye, FolderPlus } from 'lucide-react' +import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { STORAGE_ROW_TYPES, STORAGE_SORT_BY, @@ -24,12 +25,13 @@ const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { columns, selectedItems, setSelectedItems, - setView, setSortBy, setSortByOrder, addNewFolderPlaceholder, } = storageExplorerStore + const snap = useStorageExplorerStateSnapshot() + const onSelectCreateFolder = (columnIndex = -1) => { addNewFolderPlaceholder(columnIndex) } @@ -80,10 +82,10 @@ const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { } arrow={} > - setView(STORAGE_VIEWS.COLUMNS)}> + snap.setView(STORAGE_VIEWS.COLUMNS)}> As columns - setView(STORAGE_VIEWS.LIST)}> + snap.setView(STORAGE_VIEWS.LIST)}> As list diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ConfirmDeleteModal.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ConfirmDeleteModal.tsx index 6f041a1de0b..1aefdb6456d 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ConfirmDeleteModal.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ConfirmDeleteModal.tsx @@ -1,6 +1,7 @@ import { noop } from 'lodash' import { useEffect, useState } from 'react' -import { Alert, Button, Modal } from 'ui' + +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { StorageItem } from '../Storage.types' interface ConfirmDeleteModalProps { @@ -10,7 +11,7 @@ interface ConfirmDeleteModalProps { onSelectDelete: () => void } -const ConfirmDeleteModal = ({ +export const ConfirmDeleteModal = ({ visible = false, selectedItemsToDelete = [], onSelectCancel = noop, @@ -42,28 +43,19 @@ const ConfirmDeleteModal = ({ } return ( - {title}} + title={{title}} size="medium" onCancel={onSelectCancel} - customFooter={ -
- - -
- } - > - - - {description} - - -
+ onConfirm={onConfirmDelete} + variant="destructive" + alert={{ + base: { variant: 'destructive' }, + title: 'This action cannot be undone', + description, + }} + /> ) } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx index a99314b9b69..dfbd8a9bc1c 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx @@ -1,7 +1,9 @@ +import { noop } from 'lodash' import { observer } from 'mobx-react-lite' import { useEffect, useRef } from 'react' -import { noop } from 'lodash' +import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' +import { cn } from 'ui' import { CONTEXT_MENU_KEYS, STORAGE_VIEWS } from '../Storage.constants' import type { StorageColumn, StorageItem, StorageItemWithColumn } from '../Storage.types' import ColumnContextMenu from './ColumnContextMenu' @@ -10,7 +12,6 @@ import FolderContextMenu from './FolderContextMenu' import ItemContextMenu from './ItemContextMenu' export interface FileExplorerProps { - view: string columns: StorageColumn[] openedFolders: StorageItem[] selectedItems: StorageItemWithColumn[] @@ -22,7 +23,6 @@ export interface FileExplorerProps { } const FileExplorer = ({ - view = STORAGE_VIEWS.COLUMNS, columns = [], openedFolders = [], selectedItems = [], @@ -33,6 +33,7 @@ const FileExplorer = ({ onColumnLoadMore = noop, }: FileExplorerProps) => { const fileExplorerRef = useRef(null) + const snap = useStorageExplorerStateSnapshot() useEffect(() => { if (fileExplorerRef) { @@ -46,18 +47,20 @@ const FileExplorer = ({ return (
- {view === STORAGE_VIEWS.COLUMNS ? ( + {snap.view === STORAGE_VIEWS.COLUMNS ? (
{columns.map((column, index) => ( ))}
- ) : view === STORAGE_VIEWS.LIST ? ( + ) : snap.view === STORAGE_VIEWS.LIST ? ( <> {columns.length > 0 && ( ) : ( -
Unknown view: {view}
+
Unknown view: {snap.view}
)}
) diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx index 3bb49eb2f85..7d768653923 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx @@ -1,15 +1,15 @@ import { Transition } from '@headlessui/react' import { get, noop, sum } from 'lodash' +import { Upload } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { useContextMenu } from 'react-contexify' -import { Checkbox } from 'ui' import InfiniteList from 'components/ui/InfiniteList' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' -import { Upload } from 'lucide-react' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' +import { Checkbox, cn } from 'ui' import { CONTEXT_MENU_KEYS, STORAGE_ROW_STATUS, @@ -55,7 +55,6 @@ const DragOverOverlay = ({ isOpen, onDragLeave, onDrop, folderIsEmpty }: any) => export interface FileExplorerColumnProps { index: number - view: STORAGE_VIEWS column: StorageColumn fullWidth?: boolean openedFolders?: StorageItem[] @@ -69,7 +68,6 @@ export interface FileExplorerColumnProps { const FileExplorerColumn = ({ index = 0, - view = STORAGE_VIEWS.COLUMNS, column, fullWidth = false, openedFolders = [], @@ -142,145 +140,133 @@ const FileExplorerColumn = ({ ) return ( - <> -
{ - const eventTarget = get(event.target, ['className'], '') - if (typeof eventTarget === 'string' && eventTarget.includes('react-contexify')) return - onSelectColumnEmptySpace(index) - }} - > - {/* Checkbox selection for select all */} - {view === STORAGE_VIEWS.COLUMNS && ( -
event.stopPropagation()} - > - {columnFiles.length > 0 ? ( - <> - -

- Select all {columnFiles.length} files -

- - ) : ( -

No files available for selection

- )} -
- )} - - {/* List Interface Header */} - {view === STORAGE_VIEWS.LIST && ( -
-
+
{ + const eventTarget = get(event.target, ['className'], '') + if (typeof eventTarget === 'string' && eventTarget.includes('react-contexify')) return + onSelectColumnEmptySpace(index) + }} + > + {/* Checkbox selection for select all */} + {snap.view === STORAGE_VIEWS.COLUMNS && ( +
event.stopPropagation()} + > + {columnFiles.length > 0 ? ( + <> -

Name

-
-

Size

-

Type

-

Created at

-

Last modified at

-
- )} +

Select all {columnFiles.length} files

+ + ) : ( +

No files available for selection

+ )} +
+ )} - {/* Shimmering loaders while fetching contents */} - {column.status === STORAGE_ROW_STATUS.LOADING && ( -
+
+ +

Name

+
+

Size

+

Type

+

Created at

+

Last modified at

+
+ )} + + {/* Shimmering loaders while fetching contents */} + {column.status === STORAGE_ROW_STATUS.LOADING && ( +
- - - + > + + + +
+ )} + + {/* Column Interface */} + (index !== 0 && index === columnItems.length ? 85 : 37)} + hasNextPage={column.status !== STORAGE_ROW_STATUS.LOADING && column.hasMoreItems} + isLoadingNextPage={column.isLoadingMoreItems} + onLoadNextPage={() => onColumnLoadMore(index, column)} + /> + + {/* Drag drop upload CTA for when column is empty */} + {!(snap.isSearching && itemSearchString.length > 0) && + column.items.length === 0 && + column.status !== STORAGE_ROW_STATUS.LOADING && ( +
+ storage-placeholder +

Drop your files here

+

+ Or upload them via the "Upload file" button above +

)} - {/* Column Interface */} - (index !== 0 && index === columnItems.length ? 85 : 37)} - hasNextPage={column.status !== STORAGE_ROW_STATUS.LOADING && column.hasMoreItems} - isLoadingNextPage={column.isLoadingMoreItems} - onLoadNextPage={() => onColumnLoadMore(index, column)} - /> + {snap.isSearching && + itemSearchString.length > 0 && + isEmpty && + column.status !== STORAGE_ROW_STATUS.LOADING && ( +
+

No results found in this folder

+

+ Your search for "{itemSearchString}" did not return any results +

+
+ )} - {/* Drag drop upload CTA for when column is empty */} - {!(snap.isSearching && itemSearchString.length > 0) && - column.items.length === 0 && - column.status !== STORAGE_ROW_STATUS.LOADING && ( -
- storage-placeholder -

Drop your files here

-

- Or upload them via the "Upload file" button above -

-
- )} + {/* Drag drop upload CTA for when column has files */} + setIsDraggedOver(false)} + onDrop={() => setIsDraggedOver(false)} + /> - {snap.isSearching && - itemSearchString.length > 0 && - isEmpty && - column.status !== STORAGE_ROW_STATUS.LOADING && ( -
-

No results found in this folder

-

- Your search for "{itemSearchString}" did not return any results -

-
- )} - - {/* Drag drop upload CTA for when column has files */} - setIsDraggedOver(false)} - onDrop={() => setIsDraggedOver(false)} - /> -
{/* List interface footer */} - {view === STORAGE_VIEWS.LIST && ( -
+ {snap.view === STORAGE_VIEWS.LIST && ( +

{formatBytes(columnItemsSize)} for {columnItems.length} items

)} - +
) } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx index e9b7bf980c9..2574c9f862b 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx @@ -162,8 +162,6 @@ const FileExplorerHeader = ({ const storageExplorerStore = useStorageStore() const { - view, - setView, columns, sortBy, setSortBy, @@ -333,7 +331,7 @@ const FileExplorerHeader = ({ ]} /> - ) : view === STORAGE_VIEWS.COLUMNS ? ( + ) : snap.view === STORAGE_VIEWS.COLUMNS ? ( ) : ( @@ -380,10 +378,10 @@ const FileExplorerHeader = ({ {VIEW_OPTIONS.map((option) => ( - setView(option.key)}> + snap.setView(option.key)}>

{option.name}

- {view === option.key && } + {snap.view === option.key && }
))} diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx index 8b6daad9cf2..b69bafc6ac6 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx @@ -1,17 +1,19 @@ import { Download, Move, Trash2, X } from 'lucide-react' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' import { Button } from 'ui' +import { downloadFile } from './StorageExplorer.utils' const FileExplorerHeaderSelection = () => { + const { ref: projectRef, bucketId } = useParams() const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') const storageExplorerStore = useStorageStore() const { selectedItems, - downloadFile, downloadSelectedFiles, clearSelectedItems, setSelectedItemsToDelete, @@ -35,7 +37,7 @@ const FileExplorerHeaderSelection = () => { type="primary" onClick={async () => { if (selectedItems.length === 1) { - await downloadFile(selectedItems[0]) + await downloadFile({ projectRef, bucketId, file: selectedItems[0] }) } else { await downloadSelectedFiles(selectedItems) } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx index 92bb5ff9f4f..d1df99ba571 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx @@ -17,6 +17,7 @@ import { import { useContextMenu } from 'react-contexify' import SVG from 'react-inlinesvg' +import { useParams } from 'common' import type { ItemRenderer } from 'components/ui/InfiniteList' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' @@ -46,7 +47,7 @@ import { } from '../Storage.constants' import { StorageItem, StorageItemWithColumn } from '../Storage.types' import FileExplorerRowEditing from './FileExplorerRowEditing' -import { copyPathToFolder } from './StorageExplorer.utils' +import { copyPathToFolder, downloadFile } from './StorageExplorer.utils' import { useCopyUrl } from './useCopyUrl' export const RowIcon = ({ @@ -111,6 +112,7 @@ const FileExplorerRow: ItemRenderer = ({ selectedItems = [], openedFolders = [], }) => { + const { ref: projectRef, bucketId } = useParams() const storageExplorerStore = useStorageStore() const { popColumnAtIndex, @@ -126,7 +128,6 @@ const FileExplorerRow: ItemRenderer = ({ setSelectedItemsToMove, setSelectedFileCustomExpiry, fetchFolderContents, - downloadFile, downloadFolder, selectRangeItems, selectedFilePreview, @@ -260,7 +261,13 @@ const FileExplorerRow: ItemRenderer = ({ { name: 'Download', icon: , - onClick: async () => await downloadFile(itemWithColumnIndex), + onClick: async () => { + await downloadFile({ + projectRef, + bucketId, + file: itemWithColumnIndex, + }) + }, }, { name: 'Separator', icon: undefined, onClick: undefined }, ] diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx index 084290a93e1..b52a350b802 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx @@ -4,10 +4,12 @@ import { observer } from 'mobx-react-lite' import { Item, Menu, Separator, Submenu } from 'react-contexify' import 'react-contexify/dist/ReactContexify.css' +import { useParams } from 'common' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' import { URL_EXPIRY_DURATION } from '../Storage.constants' import { StorageItemWithColumn } from '../Storage.types' +import { downloadFile } from './StorageExplorer.utils' import { useCopyUrl } from './useCopyUrl' interface ItemContextMenuProps { @@ -15,9 +17,9 @@ interface ItemContextMenuProps { } const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { + const { ref: projectRef, bucketId } = useParams() const storageExplorerStore = useStorageStore() const { - downloadFile, selectedBucket, setSelectedItemsToDelete, setSelectedItemToRename, @@ -39,7 +41,7 @@ const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { case 'move': return setSelectedItemsToMove([item]) case 'download': - return await downloadFile(item) + return await downloadFile({ projectRef, bucketId, file: item }) default: break } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx index 0f89945c81c..1374b522a66 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx @@ -4,6 +4,7 @@ import { isEmpty } from 'lodash' import { AlertCircle, ChevronDown, Clipboard, Download, Loader, Trash2, X } from 'lucide-react' import SVG from 'react-inlinesvg' +import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' @@ -18,6 +19,7 @@ import { } from 'ui' import { URL_EXPIRY_DURATION } from '../Storage.constants' import { StorageItem } from '../Storage.types' +import { downloadFile } from './StorageExplorer.utils' import { useCopyUrl } from './useCopyUrl' import { useFetchFileUrlQuery } from './useFetchFileUrlQuery' @@ -115,9 +117,9 @@ const PreviewFile = ({ item }: { item: StorageItem }) => { } const PreviewPane = () => { + const { ref: projectRef, bucketId } = useParams() const storageExplorerStore = useStorageStore() const { - downloadFile, selectedBucket, selectedFilePreview: file, closeFilePreview, @@ -128,9 +130,7 @@ const PreviewPane = () => { const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') - if (!file) { - return null - } + if (!file) return null const width = 450 const isOpen = !isEmpty(file) @@ -211,7 +211,7 @@ const PreviewPane = () => { type="default" icon={} disabled={file.isCorrupted} - onClick={async () => await downloadFile(file)} + onClick={async () => await downloadFile({ projectRef, bucketId, file })} > Download diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx index e55cb48a772..020eacb45ab 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx @@ -1,14 +1,15 @@ -import { useParams } from 'common' import { compact, get, isEmpty, uniqBy } from 'lodash' import { observer } from 'mobx-react-lite' import { useEffect, useRef, useState } from 'react' +import { useParams } from 'common' import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import type { Bucket } from 'data/storage/buckets-query' import { IS_PLATFORM } from 'lib/constants' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' +import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants' -import ConfirmDeleteModal from './ConfirmDeleteModal' +import { ConfirmDeleteModal } from './ConfirmDeleteModal' import CustomExpiryModal from './CustomExpiryModal' import FileExplorer from './FileExplorer' import FileExplorerHeader from './FileExplorerHeader' @@ -35,7 +36,6 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { popOpenedFoldersAtIndex, selectedItemsToMove, clearSelectedItemsToMove, - view, currentBucketName, openBucket, @@ -49,9 +49,9 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { moveFiles, } = storageExplorerStore - const storageExplorerRef = useRef(null) - const { ref } = useParams() + const storageExplorerRef = useRef(null) + const snap = useStorageExplorerStateSnapshot() // [Joshen] This is to ensure that StorageExplorerStore can get the storage file size limit // Will be better once we deprecate the mobx store entirely, which we will get there @@ -68,7 +68,7 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { const fetchContents = async () => { - if (view === STORAGE_VIEWS.LIST) { + if (snap.view === STORAGE_VIEWS.LIST) { const currentFolderIdx = openedFolders.length - 1 const currentFolder = openedFolders[currentFolderIdx] @@ -92,7 +92,7 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { await fetchFolderContents(currentFolder.id, currentFolder.name, currentFolderIdx) } } - } else if (view === STORAGE_VIEWS.COLUMNS) { + } else if (snap.view === STORAGE_VIEWS.COLUMNS) { const paths = openedFolders.map((folder) => folder.name) fetchFoldersByPath(paths, itemSearchString, true) } @@ -102,9 +102,9 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { }, [itemSearchString]) useEffect(() => { - // Load user preferences (view and sort) + // Load user preferences (view, sort, sortBy) loadExplorerPreferences() - }, []) + }, [ref]) useEffect(() => { openBucket(bucket) @@ -196,7 +196,6 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { )}
{ - const folders = openedFolders.slice(0, item.columnIndex).map((folder) => folder.name) - const path = folders.length > 0 ? `${folders.join('/')}/${item.name}` : item.name - copyToClipboard(path) - toast.success(`Copied path to folder "${item.name}"`) -} diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.tsx new file mode 100644 index 00000000000..3d8dbf518ad --- /dev/null +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.tsx @@ -0,0 +1,87 @@ +import { downloadBucketObject } from 'data/storage/bucket-object-download-mutation' +import { copyToClipboard } from 'lib/helpers' +import { toast } from 'sonner' +import { SONNER_DEFAULT_DURATION } from 'ui' +import { StorageItem, StorageItemWithColumn } from '../Storage.types' + +export const copyPathToFolder = (openedFolders: StorageItem[], item: StorageItemWithColumn) => { + const folders = openedFolders.slice(0, item.columnIndex).map((folder) => folder.name) + const path = folders.length > 0 ? `${folders.join('/')}/${item.name}` : item.name + copyToClipboard(path) + toast.success(`Copied path to folder "${item.name}"`) +} + +export const formatTime = (seconds: number) => { + const days = Math.floor(seconds / (24 * 3600)) + seconds %= 24 * 3600 + const hours = Math.floor(seconds / 3600) + seconds %= 3600 + const minutes = Math.floor(seconds / 60) + seconds = Math.floor(seconds % 60) + + if (days > 0) return `${days}d ` + if (hours > 0) return `${hours}h ` + if (minutes > 0) return `${minutes}m ` + return `${seconds}s` +} + +export const downloadFile = async ({ + projectRef, + bucketId, + file, + showToast = true, + returnBlob = false, +}: { + projectRef?: string + bucketId?: string + file: StorageItemWithColumn + showToast?: boolean + returnBlob?: boolean +}) => { + if (!projectRef) return toast.error('Failed to download: Project ref is required') + if (!bucketId) return toast.error('Failed to download: Bucket ID is required') + if (!file.path) return toast.error('Failed to download: Unable to find path to file') + + const fileName: string = file.name + const fileMimeType = file?.metadata?.mimetype ?? undefined + + const toastId = showToast ? toast.loading(`Retrieving ${fileName}...`) : undefined + + try { + const data = await downloadBucketObject({ + projectRef, + bucketId, + path: file.path, + }) + + const blob = await data.blob() + const newBlob = new Blob([blob], { type: fileMimeType }) + if (returnBlob) return { name: fileName, blob: newBlob } + + const blobUrl = window.URL.createObjectURL(newBlob) + const link = document.createElement('a') + link.href = blobUrl + link.setAttribute('download', `${fileName}`) + document.body.appendChild(link) + link.click() + link.parentNode?.removeChild(link) + window.URL.revokeObjectURL(blob) + if (toastId) { + toast.success(`Downloading ${fileName}`, { + id: toastId, + closeButton: true, + duration: SONNER_DEFAULT_DURATION, + }) + } + return true + } catch (err) { + if (toastId) { + toast.error(`Failed to download ${fileName}`, { + id: toastId, + closeButton: true, + duration: SONNER_DEFAULT_DURATION, + }) + } + return false + } +} diff --git a/apps/studio/components/ui/Charts/ComposedChart.tsx b/apps/studio/components/ui/Charts/ComposedChart.tsx index 7ebfa5db0f6..1e6c62bbf1f 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.tsx @@ -1,21 +1,21 @@ 'use client' -import { useState, ComponentProps, useEffect } from 'react' import dayjs from 'dayjs' +import { useTheme } from 'next-themes' +import { ComponentProps, useEffect, useState } from 'react' import { Area, Bar, - ComposedChart as RechartComposedChart, - XAxis, - YAxis, CartesianGrid, Line, + ComposedChart as RechartComposedChart, ReferenceArea, Tooltip, + XAxis, + YAxis, } from 'recharts' import { CategoricalChartState } from 'recharts/types/chart/types' import { cn } from 'ui' -import { useTheme } from 'next-themes' import ChartHeader from './ChartHeader' import ChartHighlightActions from './ChartHighlightActions' import { @@ -24,17 +24,17 @@ import { STACKED_CHART_COLORS, updateStackedChartColors, } from './Charts.constants' -import { Datum, CommonChartProps } from './Charts.types' -import { useChartSize, numberFormatter } from './Charts.utils' -import { ChartHighlight } from './useChartHighlight' -import NoDataPlaceholder from './NoDataPlaceholder' -import { MultiAttribute } from './ComposedChartHandler' +import { CommonChartProps, Datum } from './Charts.types' +import { numberFormatter, useChartSize } from './Charts.utils' import { calculateTotalChartAggregate, CustomLabel, CustomTooltip, formatBytes, } from './ComposedChart.utils' +import { MultiAttribute } from './ComposedChartHandler' +import NoDataPlaceholder from './NoDataPlaceholder' +import { ChartHighlight } from './useChartHighlight' export interface BarChartProps extends CommonChartProps { attributes: MultiAttribute[] diff --git a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx index 3a6e83663bc..bacb440bf9b 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx @@ -1,12 +1,11 @@ 'use client' -import { useState } from 'react' import dayjs from 'dayjs' +import { useState } from 'react' +import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { DateTimeFormats } from './Charts.constants' import { numberFormatter } from './Charts.utils' import { MultiAttribute } from './ComposedChartHandler' -import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' -import { cn } from 'ui' interface CustomIconProps { color: string @@ -232,4 +231,4 @@ const CustomLabel = ({ payload, attributes, showMaxValue, onLabelHover }: Custom ) } -export { CustomTooltip, CustomLabel } +export { CustomLabel, CustomTooltip } diff --git a/apps/studio/components/ui/FilterPopover.tsx b/apps/studio/components/ui/FilterPopover.tsx index d3eed31644a..186d4c3c652 100644 --- a/apps/studio/components/ui/FilterPopover.tsx +++ b/apps/studio/components/ui/FilterPopover.tsx @@ -83,7 +83,7 @@ export const FilterPopover = >({
- +
{title ?? `Select ${name.toLowerCase()}`} diff --git a/apps/studio/components/ui/GroupsTelemetry.tsx b/apps/studio/components/ui/GroupsTelemetry.tsx index 5925692f290..f76ff65466e 100644 --- a/apps/studio/components/ui/GroupsTelemetry.tsx +++ b/apps/studio/components/ui/GroupsTelemetry.tsx @@ -9,7 +9,6 @@ import { useSendGroupsResetMutation } from 'data/telemetry/send-groups-reset-mut import { usePrevious } from 'hooks/deprecated' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants' -import { useAppStateSnapshot } from 'state/app-state' const getAnonId = async (id: string) => { const encoder = new TextEncoder() @@ -29,7 +28,6 @@ const GroupsTelemetry = ({ hasAcceptedConsent }: { hasAcceptedConsent: boolean } const user = useUser() const router = useRouter() const { ref, slug } = useParams() - const snap = useAppStateSnapshot() const organization = useSelectedOrganization() const previousPathname = usePrevious(router.pathname) @@ -41,13 +39,6 @@ const GroupsTelemetry = ({ hasAcceptedConsent }: { hasAcceptedConsent: boolean } const referrer = typeof document !== 'undefined' ? document?.referrer : '' useTelemetryCookie({ hasAcceptedConsent, title, referrer }) - useEffect(() => { - if (hasAcceptedConsent) { - snap.setIsOptedInTelemetry(hasAcceptedConsent) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hasAcceptedConsent]) - useEffect(() => { // don't set the sentry user id if the user hasn't logged in (so that Sentry errors show null user id instead of anonymous id) if (!user?.id) { diff --git a/apps/studio/components/ui/QueryBlock/QueryBlock.tsx b/apps/studio/components/ui/QueryBlock/QueryBlock.tsx index 0d1101e3efd..b2d5fa639c4 100644 --- a/apps/studio/components/ui/QueryBlock/QueryBlock.tsx +++ b/apps/studio/components/ui/QueryBlock/QueryBlock.tsx @@ -1,6 +1,6 @@ import { Code, Play } from 'lucide-react' import { DragEvent, ReactNode, useEffect, useMemo, useState } from 'react' -import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts' +import { Bar, BarChart, CartesianGrid, Cell, Tooltip, XAxis, YAxis } from 'recharts' import { toast } from 'sonner' import { useParams } from 'common' @@ -9,11 +9,13 @@ import { ChartConfig } from 'components/interfaces/SQLEditor/UtilityPanel/ChartC import Results from 'components/interfaces/SQLEditor/UtilityPanel/Results' import { usePrimaryDatabase } from 'data/read-replicas/replicas-query' import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' +import dayjs from 'dayjs' import { Parameter, parseParameters } from 'lib/sql-parameters' import { Dashboards } from 'types' -import { ChartContainer, ChartTooltip, ChartTooltipContent, cn, CodeBlock, SQL_ICON } from 'ui' +import { ChartContainer, ChartTooltipContent, cn, CodeBlock, SQL_ICON } from 'ui' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { ButtonTooltip } from '../ButtonTooltip' +import { CHART_COLORS } from '../Charts/Charts.constants' import SqlWarningAdmonition from '../SqlWarningAdmonition' import { BlockViewConfiguration } from './BlockViewConfiguration' import { EditQueryButton } from './EditQueryButton' @@ -129,13 +131,19 @@ export const QueryBlock = ({ const [readOnlyError, setReadOnlyError] = useState(false) const [queryError, setQueryError] = useState() const [queryResult, setQueryResult] = useState(results) + const [focusDataIndex, setFocusDataIndex] = useState() const formattedQueryResult = useMemo(() => { // Make sure Y axis values are numbers return queryResult?.map((row) => { - return Object.fromEntries(Object.entries(row).map(([key, value]) => [key, Number(value)])) + return Object.fromEntries( + Object.entries(row).map(([key, value]) => { + if (key === yKey) return [key, Number(value)] + else return [key, value] + }) + ) }) - }, [queryResult]) + }, [queryResult, yKey]) const [parameterValues, setParameterValues] = useState>({}) const [showWarning, setShowWarning] = useState<'hasWriteOperation' | 'hasUnknownFunctions'>() @@ -151,6 +159,10 @@ export const QueryBlock = ({ const postgresConnectionString = primaryDatabase?.connectionString const readOnlyConnectionString = primaryDatabase?.connection_string_read_only + const chartData = chartSettings.cumulative + ? getCumulativeResults({ rows: formattedQueryResult ?? [] }, chartSettings) + : formattedQueryResult + const { mutate: execute, isLoading: isExecuting } = useExecuteSqlMutation({ onSuccess: (data) => { onResults?.(data.result) @@ -166,6 +178,14 @@ export const QueryBlock = ({ }, }) + const getDateFormat = (key: any) => { + const value = chartData?.[0]?.[key] || '' + if (typeof value === 'number') return 'number' + if (dayjs(value).isValid()) return 'date' + return 'string' + } + const xKeyDateFormat = getDateFormat(xKey) + const handleExecute = () => { if (!sql || isLoading) return @@ -361,10 +381,9 @@ export const QueryBlock = ({

Select columns for the X and Y axes

) : ( -
+
{ + if (e.activeTooltipIndex !== focusDataIndex) { + setFocusDataIndex(e.activeTooltipIndex) + } + }} + onMouseLeave={() => setFocusDataIndex(undefined)} > - + + xKeyDateFormat === 'date' ? dayjs(value).format('MMM D YYYY HH:mm') : value + } /> - } /> - + + } /> + + {chartData?.map((_: any, index: number) => ( + + ))} +
diff --git a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts index 6333c7fae09..398a5160ac8 100644 --- a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts +++ b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts @@ -1,5 +1,5 @@ interface ResourceWarningMessage { - // should match pathnames, ex: ('/', 'project/[ref]/auth', 'project/[ref]/database', '/project/[ref]/settings/auth') + // should match pathnames, ex: ('/', 'project/[ref]/auth', 'project/[ref]/database', '/project/[ref]/settings/api') restrictToRoutes?: string[] bannerContent: { diff --git a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx index 583872abb20..60c1ce45da7 100644 --- a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx +++ b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx @@ -73,8 +73,8 @@ export const ResourceExhaustionWarningBanner = () => { null: '/project/[ref]/settings/[infra-path]', disk_space: '/project/[ref]/settings/compute-and-disk', read_only: '/project/[ref]/settings/compute-and-disk', - auth_email_rate_limit: '/project/[ref]/settings/auth', - auth_restricted_email_sending: '/project/[ref]/settings/auth', + auth_email_rate_limit: '/project/[ref]/auth/rate-limits', + auth_restricted_email_sending: '/project/[ref]/auth/smtp', default: (metric: string) => `/project/[ref]/settings/[infra-path]#${metric}`, } diff --git a/apps/studio/components/ui/SchemaSelector.tsx b/apps/studio/components/ui/SchemaSelector.tsx index df05ba0ceba..7a0293ca219 100644 --- a/apps/studio/components/ui/SchemaSelector.tsx +++ b/apps/studio/components/ui/SchemaSelector.tsx @@ -123,9 +123,10 @@ const SchemaSelector = ({ diff --git a/apps/studio/data/analytics/org-daily-stats-query.ts b/apps/studio/data/analytics/org-daily-stats-query.ts index 7c267ea047c..ee5788aa353 100644 --- a/apps/studio/data/analytics/org-daily-stats-query.ts +++ b/apps/studio/data/analytics/org-daily-stats-query.ts @@ -56,6 +56,14 @@ export enum ComputeUsageMetric { COMPUTE_HOURS_8XL = 'COMPUTE_HOURS_8XL', COMPUTE_HOURS_12XL = 'COMPUTE_HOURS_12XL', COMPUTE_HOURS_16XL = 'COMPUTE_HOURS_16XL', + COMPUTE_HOURS_24XL = 'COMPUTE_HOURS_24XL', + COMPUTE_HOURS_24XL_OPTIMIZED_CPU = 'COMPUTE_HOURS_24XL_OPTIMIZED_CPU', + COMPUTE_HOURS_24XL_OPTIMIZED_MEMORY = 'COMPUTE_HOURS_24XL_OPTIMIZED_MEMORY', + COMPUTE_HOURS_24XL_HIGH_MEMORY = 'COMPUTE_HOURS_24XL_HIGH_MEMORY', + COMPUTE_HOURS_48XL = 'COMPUTE_HOURS_48XL', + COMPUTE_HOURS_48XL_OPTIMIZED_CPU = 'COMPUTE_HOURS_48XL_OPTIMIZED_CPU', + COMPUTE_HOURS_48XL_OPTIMIZED_MEMORY = 'COMPUTE_HOURS_48XL_OPTIMIZED_MEMORY', + COMPUTE_HOURS_48XL_HIGH_MEMORY = 'COMPUTE_HOURS_48XL_HIGH_MEMORY', } export const computeUsageMetricLabel = (computeUsageMetric: ComputeUsageMetric) => { @@ -82,6 +90,22 @@ export const computeUsageMetricLabel = (computeUsageMetric: ComputeUsageMetric) return '12XL' case 'COMPUTE_HOURS_16XL': return '16XL' + case 'COMPUTE_HOURS_24XL': + return '24XL' + case 'COMPUTE_HOURS_24XL_OPTIMIZED_CPU': + return '24XL - Optimized CPU' + case 'COMPUTE_HOURS_24XL_OPTIMIZED_MEMORY': + return '24XL - Optimized Memory' + case 'COMPUTE_HOURS_24XL_HIGH_MEMORY': + return '24XL - High Memory' + case 'COMPUTE_HOURS_48XL': + return '48XL' + case 'COMPUTE_HOURS_48XL_OPTIMIZED_CPU': + return '48XL - Optimized CPU' + case 'COMPUTE_HOURS_48XL_OPTIMIZED_MEMORY': + return '48XL - Optimized Memory' + case 'COMPUTE_HOURS_48XL_HIGH_MEMORY': + return '48XL - High Memory' } } diff --git a/apps/studio/data/analytics/project-log-stats-query.ts b/apps/studio/data/analytics/project-log-stats-query.ts index db6f085c46f..76167f33d9e 100644 --- a/apps/studio/data/analytics/project-log-stats-query.ts +++ b/apps/studio/data/analytics/project-log-stats-query.ts @@ -5,7 +5,9 @@ import { analyticsKeys } from './keys' export type ProjectLogStatsVariables = { projectRef?: string - interval?: operations['UsageApiController_getApiCounts']['parameters']['query']['interval'] + interval?: NonNullable< + operations['UsageApiController_getApiCounts']['parameters']['query'] + >['interval'] } export type ProjectLogStatsResponse = { diff --git a/apps/studio/data/database/backups-query.ts b/apps/studio/data/database/backups-query.ts index 626ffef968d..7cef5a9462a 100644 --- a/apps/studio/data/database/backups-query.ts +++ b/apps/studio/data/database/backups-query.ts @@ -2,7 +2,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' import type { components } from 'data/api' import { get, handleError } from 'data/fetchers' -import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' +import { useIsOrioleDbInAws } from 'hooks/misc/useSelectedProject' import type { ResponseError } from 'types' import { databaseKeys } from './keys' @@ -32,11 +32,14 @@ export const useBackupsQuery = ( { enabled = true, ...options }: UseQueryOptions = {} ) => { // [Joshen] Check for specifically false to account for project not loaded yet - const isOrioleDb = useIsOrioleDb() + const isOrioleDbInAws = useIsOrioleDbInAws() return useQuery( databaseKeys.backups(projectRef), ({ signal }) => getBackups({ projectRef }, signal), - { enabled: enabled && isOrioleDb === false && typeof projectRef !== 'undefined', ...options } + { + enabled: enabled && !isOrioleDbInAws && typeof projectRef !== 'undefined', + ...options, + } ) } diff --git a/apps/studio/data/integrations/integrations-query.ts b/apps/studio/data/integrations/integrations-query.ts index 752c452b2e0..e922fb4042a 100644 --- a/apps/studio/data/integrations/integrations-query.ts +++ b/apps/studio/data/integrations/integrations-query.ts @@ -6,7 +6,6 @@ import { integrationKeys } from './keys' export async function getIntegrations(signal?: AbortSignal) { const { data, error } = await get('/platform/integrations', { - params: { query: { integration_name: '' } }, signal, }) diff --git a/apps/studio/data/invoices/invoices-query.ts b/apps/studio/data/invoices/invoices-query.ts index 88f8699d40a..23747215b08 100644 --- a/apps/studio/data/invoices/invoices-query.ts +++ b/apps/studio/data/invoices/invoices-query.ts @@ -18,7 +18,7 @@ export async function getInvoices( const { data, error } = await get(`/platform/organizations/{slug}/billing/invoices`, { params: { path: { slug }, - query: { offset: offset.toString(), limit: limit.toString() }, + query: { offset, limit }, }, signal, }) diff --git a/apps/studio/data/misc/get-default-region-query.ts b/apps/studio/data/misc/get-default-region-query.ts index af9f135f336..d5e5a81ce10 100644 --- a/apps/studio/data/misc/get-default-region-query.ts +++ b/apps/studio/data/misc/get-default-region-query.ts @@ -12,7 +12,7 @@ import type { CloudProvider } from 'shared-data' import { AWS_REGIONS, FLY_REGIONS } from 'shared-data' const RESTRICTED_POOL = [ - 'EAST_US', + 'EAST_US_2', 'CENTRAL_EU', 'NORTH_EU', 'WEST_EU', diff --git a/apps/studio/data/notifications/notifications-v2-query.ts b/apps/studio/data/notifications/notifications-v2-query.ts index 0aec4235e7c..60df39adb79 100644 --- a/apps/studio/data/notifications/notifications-v2-query.ts +++ b/apps/studio/data/notifications/notifications-v2-query.ts @@ -40,8 +40,8 @@ export async function getNotifications(options: NotificationVariables, signal?: const { data, error } = await get('/platform/notifications', { params: { query: { - offset: String(page * limit), - limit: String(limit), + offset: page * limit, + limit, // [Alaister]: 'as any' is needed because the API types don't reflect an array of strings ...(status !== undefined ? { status } : { status: ['new', 'seen'].join(',') as any }), ...(filters.priority.length > 0 ? { priority: filters.priority.join(',') as any } : {}), diff --git a/apps/studio/data/organization-members/organization-member-role-unassign-mutation.ts b/apps/studio/data/organization-members/organization-member-role-unassign-mutation.ts index f486d86c99b..c7d90609118 100644 --- a/apps/studio/data/organization-members/organization-member-role-unassign-mutation.ts +++ b/apps/studio/data/organization-members/organization-member-role-unassign-mutation.ts @@ -25,7 +25,7 @@ export async function unassignOrganizationMemberRole({ path: { slug, gotrue_id: gotrueId, - role_id: roleId.toString(), + role_id: roleId, }, }, } diff --git a/apps/studio/data/organization-members/organization-member-role-update-mutation.ts b/apps/studio/data/organization-members/organization-member-role-update-mutation.ts index 645337d6ede..0866ae2d4ca 100644 --- a/apps/studio/data/organization-members/organization-member-role-update-mutation.ts +++ b/apps/studio/data/organization-members/organization-member-role-update-mutation.ts @@ -25,7 +25,7 @@ export async function assignOrganizationMemberRole({ const { data, error } = await put( '/platform/organizations/{slug}/members/{gotrue_id}/roles/{role_id}', { - params: { path: { slug, gotrue_id: gotrueId, role_id: roleId.toString() } }, + params: { path: { slug, gotrue_id: gotrueId, role_id: roleId } }, body: { name: roleName, role_scoped_projects: projects }, } ) diff --git a/apps/studio/data/organizations/organization-create-mutation.ts b/apps/studio/data/organizations/organization-create-mutation.ts index 77842d34cea..acd8c924c12 100644 --- a/apps/studio/data/organizations/organization-create-mutation.ts +++ b/apps/studio/data/organizations/organization-create-mutation.ts @@ -5,6 +5,7 @@ import { handleError, post } from 'data/fetchers' import { permissionKeys } from 'data/permissions/keys' import type { ResponseError } from 'types' import { organizationKeys } from './keys' +import type { components } from 'data/api' export type OrganizationCreateVariables = { name: string @@ -12,6 +13,9 @@ export type OrganizationCreateVariables = { size?: string tier: 'tier_payg' | 'tier_pro' | 'tier_free' | 'tier_team' | 'tier_enterprise' payment_method?: string + billing_name?: string + address?: components['schemas']['CreateOrganizationBody']['address'] + tax_id?: components['schemas']['CreateOrganizationBody']['tax_id'] } export async function createOrganization({ @@ -20,15 +24,20 @@ export async function createOrganization({ size, tier, payment_method, + address, + billing_name, + tax_id, }: OrganizationCreateVariables) { const { data, error } = await post('/platform/organizations', { body: { name, - // @ts-ignore [Joshen] Generated API spec is wrong kind, size, tier, payment_method, + address, + billing_name, + tax_id, }, }) diff --git a/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts b/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts index ca7af88b5d8..ae62b58a1df 100644 --- a/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts +++ b/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts @@ -7,13 +7,15 @@ import type { ResponseError } from 'types' import { organizationKeys } from './keys' export type OrganizationCustomerProfileUpdateVariables = { - slug: string + slug?: string address?: components['schemas']['CustomerResponse']['address'] + billing_name: string } export async function updateOrganizationCustomerProfile({ slug, address, + billing_name, }: OrganizationCustomerProfileUpdateVariables) { if (!slug) return console.error('Slug is required') @@ -26,7 +28,7 @@ export async function updateOrganizationCustomerProfile({ slug, }, }, - body: { address }, + body: { address: address != null ? address : undefined, billing_name }, }) if (error) throw handleError(error) return data @@ -56,13 +58,14 @@ export const useOrganizationCustomerProfileUpdateMutation = ({ OrganizationCustomerProfileUpdateVariables >((vars) => updateOrganizationCustomerProfile(vars), { async onSuccess(data, variables, context) { - const { address, slug } = variables + const { address, slug, billing_name } = variables // We do not invalidate here as GET endpoint data is stale for 1-2 seconds, so we handle state manually queryClient.setQueriesData(organizationKeys.customerProfile(slug), (prev: any) => { if (!prev) return prev return { ...prev, + billing_name, address, } }) diff --git a/apps/studio/data/organizations/organization-tax-id-query.ts b/apps/studio/data/organizations/organization-tax-id-query.ts index 6f47c864250..e672b0611aa 100644 --- a/apps/studio/data/organizations/organization-tax-id-query.ts +++ b/apps/studio/data/organizations/organization-tax-id-query.ts @@ -20,14 +20,10 @@ export async function getOrganizationTaxId( const { data, error } = await get(`/platform/organizations/{slug}/tax-ids`, { params: { path: { slug } }, signal, - headers: { - Version: '2', - }, }) if (error) throw handleError(error) - // @ts-ignore wrong typing due to mgmt api versioning - return (data as components['schemas']['TaxIdV2Response']).tax_id + return (data as components['schemas']['TaxIdResponse']).tax_id } export type OrganizationTaxIdData = Awaited> diff --git a/apps/studio/data/organizations/organization-tax-id-update-mutation.ts b/apps/studio/data/organizations/organization-tax-id-update-mutation.ts index 0af1701d696..14bec45b564 100644 --- a/apps/studio/data/organizations/organization-tax-id-update-mutation.ts +++ b/apps/studio/data/organizations/organization-tax-id-update-mutation.ts @@ -6,7 +6,7 @@ import type { ResponseError } from 'types' import { organizationKeys } from './keys' export type OrganizationTaxIdUpdateVariables = { - slug: string + slug?: string taxId: { type: string; value: string; country?: string } | null } diff --git a/apps/studio/data/projects/new-project.constants.ts b/apps/studio/data/projects/new-project.constants.ts index ecdaf43fee9..98240ddd5dd 100644 --- a/apps/studio/data/projects/new-project.constants.ts +++ b/apps/studio/data/projects/new-project.constants.ts @@ -105,4 +105,68 @@ export const instanceSizeSpecs: Record< priceMonthly: 3730, cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], }, + '24xlarge': { + label: '24XL', + ram: '384 GB', + cpu: '96-core', + priceHourly: 9.73, + priceMonthly: 7100, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '24xlarge_optimized_cpu': { + label: '24XL - Optimized CPU', + ram: '192 GB', + cpu: '96-core', + priceHourly: 8.9, + priceMonthly: 6500, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '24xlarge_optimized_memory': { + label: '24XL - Optimized Memory', + ram: '768 GB', + cpu: '96-core', + priceHourly: 13.84, + priceMonthly: 10100, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '24xlarge_high_memory': { + label: '24XL - High Memory', + ram: '1536 GB', + cpu: '96-core', + priceHourly: 21.91, + priceMonthly: 16000, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '48xlarge': { + label: '48XL', + ram: '768 GB', + cpu: '192-core', + priceHourly: 19.47, + priceMonthly: 14200, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '48xlarge_optimized_cpu': { + label: '48XL - Optimized CPU', + ram: '384 GB', + cpu: '192-core', + priceHourly: 17.8, + priceMonthly: 13000, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '48xlarge_optimized_memory': { + label: '48XL - Optimized Memory', + ram: '1536 GB', + cpu: '192-core', + priceHourly: 27.68, + priceMonthly: 20200, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '48xlarge_high_memory': { + label: '48XL - High Memory', + ram: '3072 GB', + cpu: '192-core', + priceHourly: 43.84, + priceMonthly: 32000, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, } diff --git a/apps/studio/data/sql/execute-sql-mutation.ts b/apps/studio/data/sql/execute-sql-mutation.ts index 01df31c8bc1..b5ddceaf427 100644 --- a/apps/studio/data/sql/execute-sql-mutation.ts +++ b/apps/studio/data/sql/execute-sql-mutation.ts @@ -1,16 +1,13 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { databaseExtensionsKeys } from 'data/database-extensions/keys' -import { databasePoliciesKeys } from 'data/database-policies/keys' -import { databaseRoleKeys } from 'data/database-roles/keys' -import { databaseTriggerKeys } from 'data/database-triggers/keys' -import { databaseKeys } from 'data/database/keys' -import { entityTypeKeys } from 'data/entity-types/keys' -import { enumeratedTypesKeys } from 'data/enumerated-types/keys' -import { tableKeys } from 'data/tables/keys' import { executeSql, ExecuteSqlData, ExecuteSqlVariables } from './execute-sql-query' +// [Joshen] Intention is that we invalidate all database related keys whenever running a mutation related query +// So we attempt to ignore all the non-related query keys. We could probably look into grouping our query keys better +// actually to not make this too hacky here +const INVALIDATION_KEYS_IGNORE = ['branches', 'settings-v2', 'addons', 'custom-domains', 'content'] + export type QueryResponseError = { code: string message: string @@ -41,9 +38,16 @@ export const useExecuteSqlMutation = ({ const { contextualInvalidation, sql, projectRef } = variables // [Joshen] Default to false for now, only used for SQL editor to dynamically invalidate - if (contextualInvalidation && projectRef) { - const invalidationKeys = inferInvalidationKeys(projectRef, sql) - await Promise.all(invalidationKeys.map((key) => queryClient.invalidateQueries(key))) + const sqlLower = sql.toLowerCase() + const isMutationSQL = + sqlLower.includes('create ') || sqlLower.includes('alter ') || sqlLower.includes('drop ') + if (contextualInvalidation && projectRef && isMutationSQL) { + const databaseRelatedKeys = queryClient + .getQueryCache() + .findAll(['projects', projectRef]) + .map((x) => x.queryKey) + .filter((x) => !INVALIDATION_KEYS_IGNORE.some((a) => x.includes(a))) + await Promise.all(databaseRelatedKeys.map((key) => queryClient.invalidateQueries(key))) } await onSuccess?.(data, variables, context) }, @@ -58,68 +62,3 @@ export const useExecuteSqlMutation = ({ } ) } - -// [Joshen] Can expand this further eventually, but just covering certain easy cases for now -const inferInvalidationKeys = (ref: string, sql: string) => { - const keys = [] - const sqlLower = sql.toLowerCase() - - if ( - sqlLower.includes('create table') || - sqlLower.includes('alter table') || - sqlLower.includes('drop table') - ) { - keys.push(entityTypeKeys.list(ref)) - keys.push(tableKeys.list(ref)) - } - if ( - sqlLower.includes('create schema') || - sqlLower.includes('alter schema') || - sqlLower.includes('drop schema') - ) { - keys.push(databaseKeys.schemas(ref)) - } - if ( - sqlLower.includes('create function') || - sqlLower.includes('alter function') || - sqlLower.includes('drop function') - ) { - keys.push(databaseKeys.databaseFunctions(ref)) - } - if ( - sqlLower.includes('create trigger') || - sqlLower.includes('alter trigger') || - sqlLower.includes('drop trigger') - ) { - keys.push(databaseTriggerKeys.list(ref)) - } - if ( - sqlLower.includes('create policy') || - sqlLower.includes('alter policy') || - sqlLower.includes('drop policy') - ) { - keys.push(databasePoliciesKeys.list(ref)) - } - if ( - sqlLower.includes('create type') || - sqlLower.includes('alter type') || - sqlLower.includes('drop type') - ) { - keys.push(enumeratedTypesKeys.list(ref)) - } - if ( - sqlLower.includes('create role') || - sqlLower.includes('alter role') || - sqlLower.includes('drop role') - ) { - keys.push(databaseRoleKeys.databaseRoles(ref)) - } - if (sqlLower.includes('create index') || sqlLower.includes('drop index')) { - keys.push(databaseKeys.indexes(ref)) - } - if (sqlLower.includes('create extension') || sqlLower.includes('drop extension')) { - keys.push(databaseExtensionsKeys.list(ref)) - } - - return keys -} diff --git a/apps/studio/data/table-editor/keys.ts b/apps/studio/data/table-editor/keys.ts index 6ce556c9754..851eefd9838 100644 --- a/apps/studio/data/table-editor/keys.ts +++ b/apps/studio/data/table-editor/keys.ts @@ -1,4 +1,4 @@ export const tableEditorKeys = { - tableEditor: (projectRef: string | undefined, id: number | undefined) => - ['projects', projectRef, 'table-editor', id] as const, + tableEditor: (projectRef: string | undefined, id?: number) => + ['projects', projectRef, 'table-editor', id].filter(Boolean), } diff --git a/apps/studio/data/table-editor/table-editor-query-sql.ts b/apps/studio/data/table-editor/table-editor-query-sql.ts index ab3684b448b..6eba0bb798c 100644 --- a/apps/studio/data/table-editor/table-editor-query-sql.ts +++ b/apps/studio/data/table-editor/table-editor-query-sql.ts @@ -162,6 +162,7 @@ export function getTableEditorSql(id?: number) { conkey[1] as ordinal_position from pg_catalog.pg_constraint where contype = 'u' and cardinality(conkey) = 1 + group by conrelid, conkey[1] ) as uniques on uniques.table_id = a.attrelid and uniques.ordinal_position = a.attnum left join ( select distinct on (conrelid, conkey[1]) diff --git a/apps/studio/data/table-editor/table-editor-types.ts b/apps/studio/data/table-editor/table-editor-types.ts index 189dd4fcbfd..b793f43111d 100644 --- a/apps/studio/data/table-editor/table-editor-types.ts +++ b/apps/studio/data/table-editor/table-editor-types.ts @@ -82,11 +82,12 @@ export function isViewLike(entity?: Entity): entity is View | MaterializedView { return isView(entity) || isMaterializedView(entity) } -export function postgresTableToEntity(table: PostgresTable): Entity { +export function postgresTableToEntity(table: PostgresTable): Entity | undefined { if (table.columns === undefined || table.relationships === undefined) { - throw new Error( - 'Unable to convert PostgresTable to Entity type. columns and relationships must not be undefined.' + console.error( + 'Unable to convert PostgresTable to Entity type: columns and relationships must not be undefined.' ) + return undefined } const tableRelationships: TableRelationship[] = table.relationships.map((rel) => ({ diff --git a/apps/studio/data/table-rows/table-rows-query.test.ts b/apps/studio/data/table-rows/table-rows-query.test.ts new file mode 100644 index 00000000000..7b3af9a0575 --- /dev/null +++ b/apps/studio/data/table-rows/table-rows-query.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it, vi } from 'vitest' +import { executeWithRetry } from './table-rows-query' + +describe('executeWithRetry', () => { + it('should return the result of the function when successful', async () => { + const mockFn = vi.fn().mockResolvedValue('success') + const result = await executeWithRetry(mockFn) + expect(result).toBe('success') + expect(mockFn).toHaveBeenCalledTimes(1) + }) + + it('should retry on 429 errors with exponential backoff', async () => { + const mockFn = vi + .fn() + .mockRejectedValueOnce({ status: 429, headers: { get: () => '1' } }) + .mockRejectedValueOnce({ status: 429, headers: { get: () => '1' } }) + .mockResolvedValue('success') + + const result = await executeWithRetry(mockFn) + expect(result).toBe('success') + expect(mockFn).toHaveBeenCalledTimes(3) + }) + + it('should throw error after max retries', async () => { + const mockFn = vi.fn().mockRejectedValue({ status: 429, headers: { get: () => '1' } }) + + await expect(executeWithRetry(mockFn, 2)).rejects.toMatchObject({ status: 429 }) + expect(mockFn).toHaveBeenCalledTimes(3) // Initial attempt + 2 retries + }) + + it('should throw non-429 errors immediately', async () => { + const mockFn = vi.fn().mockRejectedValue(new Error('Some other error')) + + await expect(executeWithRetry(mockFn)).rejects.toThrow('Some other error') + expect(mockFn).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/studio/data/table-rows/table-rows-query.ts b/apps/studio/data/table-rows/table-rows-query.ts index 56825ec17d7..cb8b15eb315 100644 --- a/apps/studio/data/table-rows/table-rows-query.ts +++ b/apps/studio/data/table-rows/table-rows-query.ts @@ -1,3 +1,5 @@ +import { Query } from '@supabase/pg-meta/src/query' +import { getTableRowsSql } from '@supabase/pg-meta/src/query/table-row-query' import { useQuery, useQueryClient, @@ -5,8 +7,6 @@ import { type UseQueryOptions, } from '@tanstack/react-query' -import { Query } from '@supabase/pg-meta/src/query' -import { getTableRowsSql } from '@supabase/pg-meta/src/query/table-row-query' import { IS_PLATFORM } from 'common' import { parseSupaTable } from 'components/grid/SupabaseGrid.utils' import { Filter, Sort, SupaRow, SupaTable } from 'components/grid/types' @@ -45,11 +45,11 @@ async function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } -async function executeWithRetry( - fn: () => Promise, +export async function executeWithRetry( + fn: () => Promise, maxRetries: number = 3, baseDelay: number = 500 -): Promise { +): Promise { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn() @@ -64,6 +64,7 @@ async function executeWithRetry( throw error } } + throw new Error('Max retries reached without success') } // TODO: fetchAllTableRows is used for CSV export, but since it doesn't actually truncate anything, (compare to getTableRows) @@ -153,7 +154,7 @@ export const fetchAllTableRows = async ({ await sleep(THROTTLE_DELAY) } catch (error) { throw new Error( - `Error fetching table rows: ${error instanceof Error ? error.message : 'Unknown error'}` + `Error fetching all table rows: ${error instanceof Error ? error.message : 'Unknown error'}` ) } } @@ -202,23 +203,28 @@ export async function getTableRows( getTableRowsSql({ table: entity, filters, sorts, limit, page }), roleImpersonationState ) - const { result } = await executeSql( - { - projectRef, - connectionString, - sql, - queryKey: ['table-rows', table?.id], - isRoleImpersonationEnabled: isRoleImpersonationEnabled(roleImpersonationState?.role), - }, - signal - ) - const rows = result.map((x: any, index: number) => { - return { idx: index, ...x } - }) as SupaRow[] + try { + const { result } = await executeSql( + { + projectRef, + connectionString, + sql, + queryKey: ['table-rows', table?.id], + isRoleImpersonationEnabled: isRoleImpersonationEnabled(roleImpersonationState?.role), + }, + signal + ) - return { - rows, + const rows = result.map((x: any, index: number) => { + return { idx: index, ...x } + }) as SupaRow[] + + return { rows } + } catch (error) { + throw new Error( + `Error fetching table rows: ${error instanceof Error ? error.message : 'Unknown error'}` + ) } } diff --git a/apps/studio/data/telemetry/send-groups-identify-mutation.ts b/apps/studio/data/telemetry/send-groups-identify-mutation.ts index 6ad7e4a534c..07d8b316fc9 100644 --- a/apps/studio/data/telemetry/send-groups-identify-mutation.ts +++ b/apps/studio/data/telemetry/send-groups-identify-mutation.ts @@ -1,7 +1,7 @@ import { useMutation, UseMutationOptions } from '@tanstack/react-query' import { components } from 'api-types' -import { LOCAL_STORAGE_KEYS } from 'common' +import { hasConsented } from 'common' import { handleError, post } from 'data/fetchers' import { IS_PLATFORM } from 'lib/constants' import type { ResponseError } from 'types' @@ -9,10 +9,7 @@ import type { ResponseError } from 'types' export type SendGroupsIdentifyVariables = components['schemas']['TelemetryGroupsIdentityBody'] export async function sendGroupsIdentify({ body }: { body: SendGroupsIdentifyVariables }) { - const consent = - (typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null) === 'true' + const consent = hasConsented() if (!consent || !IS_PLATFORM) return undefined diff --git a/apps/studio/data/telemetry/send-groups-reset-mutation.ts b/apps/studio/data/telemetry/send-groups-reset-mutation.ts index 28805372b52..6f8524547f4 100644 --- a/apps/studio/data/telemetry/send-groups-reset-mutation.ts +++ b/apps/studio/data/telemetry/send-groups-reset-mutation.ts @@ -1,7 +1,7 @@ import { useMutation, UseMutationOptions } from '@tanstack/react-query' import { components } from 'api-types' -import { LOCAL_STORAGE_KEYS } from 'common' +import { hasConsented } from 'common' import { handleError, post } from 'data/fetchers' import { IS_PLATFORM } from 'lib/constants' import type { ResponseError } from 'types' @@ -9,10 +9,7 @@ import type { ResponseError } from 'types' export type SendGroupsResetVariables = components['schemas']['TelemetryGroupsResetBody'] export async function sendGroupsReset({ body }: { body: SendGroupsResetVariables }) { - const consent = - (typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null) === 'true' + const consent = hasConsented() if (!consent || !IS_PLATFORM) return undefined diff --git a/apps/studio/hooks/misc/useCheckPermissions.ts b/apps/studio/hooks/misc/useCheckPermissions.ts index bbff7a730b1..3c9e5597d81 100644 --- a/apps/studio/hooks/misc/useCheckPermissions.ts +++ b/apps/studio/hooks/misc/useCheckPermissions.ts @@ -3,11 +3,11 @@ import jsonLogic from 'json-logic-js' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { usePermissionsQuery } from 'data/permissions/permissions-query' +import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { IS_PLATFORM } from 'lib/constants' import type { Permission } from 'types' import { useSelectedOrganization } from './useSelectedOrganization' import { useSelectedProject } from './useSelectedProject' -import { useProjectDetailQuery } from 'data/projects/project-detail-query' const toRegexpString = (actionOrResource: string) => `^${actionOrResource.replace('.', '\\.').replace('%', '.*')}$` @@ -80,12 +80,11 @@ export function useGetProjectPermissions( projectRefOverride?: string, enabled = true ) { - const permissionsResult = usePermissionsQuery({ + const { data, isLoading, isSuccess } = usePermissionsQuery({ enabled: permissionsOverride === undefined && enabled, }) - const permissions = - permissionsOverride === undefined ? permissionsResult.data : permissionsOverride + const permissions = permissionsOverride === undefined ? data : permissionsOverride const organizationResult = useSelectedOrganization({ enabled: organizationSlugOverride === undefined && enabled, @@ -109,7 +108,8 @@ export function useGetProjectPermissions( permissions, organizationSlug, projectRef, - isLoading: permissionsResult.isLoading, + isLoading, + isSuccess, } } @@ -193,21 +193,25 @@ export function useAsyncCheckProjectPermissions( organizationSlug: _organizationSlug, projectRef: _projectRef, isLoading: isPermissionsLoading, + isSuccess: isPermissionsSuccess, } = useGetProjectPermissions(permissions, organizationSlug, projectRef, isLoggedIn) if (!isLoggedIn) return { - isLoading: false, + isLoading: true, + isSuccess: false, can: false, } if (!IS_PLATFORM) return { isLoading: false, + isSuccess: true, can: true, } return { isLoading: isPermissionsLoading, + isSuccess: isPermissionsSuccess, can: doPermissionsCheck(allPermissions, action, resource, data, _organizationSlug, _projectRef), } } diff --git a/apps/studio/hooks/misc/useSelectedProject.ts b/apps/studio/hooks/misc/useSelectedProject.ts index 7fb9b5d60e8..c973f22e083 100644 --- a/apps/studio/hooks/misc/useSelectedProject.ts +++ b/apps/studio/hooks/misc/useSelectedProject.ts @@ -1,7 +1,9 @@ +import { useMemo } from 'react' + import { useIsLoggedIn, useParams } from 'common' import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { ProjectInfo, useProjectsQuery } from 'data/projects/projects-query' -import { useMemo } from 'react' +import { PROVIDERS } from 'lib/constants' export function useSelectedProject({ enabled = true } = {}) { const { ref } = useParams() @@ -37,3 +39,10 @@ export const useIsOrioleDb = () => { const isOrioleDb = project?.dbVersion?.endsWith('orioledb') return isOrioleDb } + +export const useIsOrioleDbInAws = () => { + const project = useSelectedProject() + const isOrioleDbInAws = + project?.dbVersion?.endsWith('orioledb') && project?.cloud_provider === PROVIDERS.AWS.id + return isOrioleDbInAws +} diff --git a/apps/studio/lib/constants/index.ts b/apps/studio/lib/constants/index.ts index 01897488056..942be406eb5 100644 --- a/apps/studio/lib/constants/index.ts +++ b/apps/studio/lib/constants/index.ts @@ -48,18 +48,16 @@ export const LOCAL_STORAGE_KEYS = { UI_PREVIEW_CLS: 'supabase-ui-cls', UI_PREVIEW_INLINE_EDITOR: 'supabase-ui-preview-inline-editor', UI_ONBOARDING_NEW_PAGE_SHOWN: 'supabase-ui-onboarding-new-page-shown', - UI_TABLE_EDITOR_TABS: 'supabase-ui-table-editor-tabs', UI_SQL_EDITOR_TABS: 'supabase-ui-sql-editor-tabs', UI_NEW_LAYOUT_PREVIEW: 'supabase-ui-new-layout-preview', - NEW_LAYOUT_NOTICE_ACKNOWLEDGED: 'new-layout-notice-acknowledge', DASHBOARD_HISTORY: (ref: string) => `dashboard-history-${ref}`, + STORAGE_PREFERENCE: (ref: string) => `storage-explorer-${ref}`, SQL_EDITOR_INTELLISENSE: 'supabase_sql-editor-intellisense-enabled', SQL_EDITOR_SPLIT_SIZE: 'supabase_sql-editor-split-size', - SQL_EDITOR_AI_PANEL_SPLIT_SIZE: 'supabase_sql-editor-ai-panel-split-size', // Key to track which schemas are ok to be sent to AI. The project ref is intentionally put at the end for easier search in the browser console. SQL_EDITOR_AI_SCHEMA: (ref: string) => `supabase_sql-editor-ai-schema-enabled-${ref}`, SQL_EDITOR_AI_OPEN: 'supabase_sql-editor-ai-open', diff --git a/apps/studio/lib/posthog.ts b/apps/studio/lib/posthog.ts index c3560627bc2..96735b0b77f 100644 --- a/apps/studio/lib/posthog.ts +++ b/apps/studio/lib/posthog.ts @@ -1,15 +1,12 @@ import { components } from 'api-types' -import { LOCAL_STORAGE_KEYS } from 'common' +import { hasConsented } from 'common' import { handleError, post } from 'data/fetchers' import { IS_PLATFORM } from './constants' type TrackFeatureFlagVariables = components['schemas']['TelemetryFeatureFlagBodyDto'] export async function trackFeatureFlag(body: TrackFeatureFlagVariables) { - const consent = - (typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null) === 'true' + const consent = hasConsented() if (!consent || !IS_PLATFORM) return undefined const { data, error } = await post(`/platform/telemetry/feature-flags/track`, { body }) diff --git a/apps/studio/lib/telemetry.tsx b/apps/studio/lib/telemetry.tsx new file mode 100644 index 00000000000..84b40e143db --- /dev/null +++ b/apps/studio/lib/telemetry.tsx @@ -0,0 +1,22 @@ +import { PageTelemetry } from 'common' +import GroupsTelemetry from 'components/ui/GroupsTelemetry' +import { API_URL, IS_PLATFORM } from 'lib/constants' +import { useConsentToast } from 'ui-patterns/consent' + +export function Telemetry() { + // Although this is "technically" breaking the rules of hooks + // IS_PLATFORM never changes within a session, so this won't cause any issues + // eslint-disable-next-line react-hooks/rules-of-hooks + const { hasAcceptedConsent } = IS_PLATFORM ? useConsentToast() : { hasAcceptedConsent: true } + + return ( + <> + + + + ) +} diff --git a/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx b/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx index 69ba342545c..63fb40418ea 100644 --- a/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx +++ b/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx @@ -29,6 +29,10 @@ import { StorageItemMetadata, StorageItemWithColumn, } from 'components/to-be-cleaned/Storage/Storage.types' +import { + downloadFile, + formatTime, +} from 'components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils' import { convertFromBytes } from 'components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils' import { configKeys } from 'data/config/keys' import { ProjectStorageConfigResponse } from 'data/config/project-storage-config-query' @@ -38,7 +42,8 @@ import { downloadBucketObject } from 'data/storage/bucket-object-download-mutati import { StorageObject, listBucketObjects } from 'data/storage/bucket-objects-list-mutation' import { Bucket } from 'data/storage/buckets-query' import { moveStorageObject } from 'data/storage/object-move-mutation' -import { IS_PLATFORM } from 'lib/constants' +import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants' +import { tryParseJson } from 'lib/helpers' import { lookupMime } from 'lib/mime' import Link from 'next/link' import { Button, SONNER_DEFAULT_DURATION, SonnerProgress } from 'ui' @@ -72,7 +77,6 @@ const STORAGE_PROGRESS_INFO_TEXT = "Do not close the browser until it's complete class StorageExplorerStore { projectRef: string = '' - view: STORAGE_VIEWS = STORAGE_VIEWS.COLUMNS sortBy: STORAGE_SORT_BY = STORAGE_SORT_BY.NAME sortByOrder: STORAGE_SORT_BY_ORDER = STORAGE_SORT_BY_ORDER.ASC // selectedBucket will get initialized with a bucket before using @@ -155,7 +159,7 @@ class StorageExplorerStore { } private getLocalStorageKey = () => { - return `supabase-storage-${this.projectRef}` + return LOCAL_STORAGE_KEYS.STORAGE_PREFERENCE(this.projectRef) } private getLatestColumnIndex = () => { @@ -190,12 +194,6 @@ class StorageExplorerStore { this.clearSelectedItems() } - setView = (view: STORAGE_VIEWS) => { - this.view = view - this.closeFilePreview() - this.updateExplorerPreferences() - } - setSortBy = async (sortBy: STORAGE_SORT_BY) => { this.sortBy = sortBy this.closeFilePreview() @@ -415,7 +413,7 @@ class StorageExplorerStore { 1 ? 's' : ''}...`} - progressPrefix={`${remainingTime && !isNaN(remainingTime) && isFinite(remainingTime) && remainingTime !== 0 ? `${this.formatTime(remainingTime)} remaining – ` : ''}`} + progressPrefix={`${remainingTime && !isNaN(remainingTime) && isFinite(remainingTime) && remainingTime !== 0 ? `${formatTime(remainingTime)} remaining – ` : ''}`} action={ toastId && (
- { } }, [selectedTable, id, projectRef, isTableEditorTabsEnabled]) - return ( - <> - - - ) + return } TableEditorPage.getLayout = (page) => ( diff --git a/apps/studio/pages/project/[ref]/editor/index.tsx b/apps/studio/pages/project/[ref]/editor/index.tsx index 2f3bebfa86f..e1a129c3808 100644 --- a/apps/studio/pages/project/[ref]/editor/index.tsx +++ b/apps/studio/pages/project/[ref]/editor/index.tsx @@ -25,8 +25,8 @@ const TableEditorPage: NextPageWithLayout = () => { } useEffect(() => { + const lastOpenedTab = appSnap.dashboardHistory.editor if (isTableEditorTabsEnabled) { - const lastOpenedTab = appSnap.dashboardHistory.editor const lastTabId = tabStore.openTabs.find((id) => editorEntityTypes.table.includes(tabStore.tabsMap[id]?.type) ) @@ -37,6 +37,8 @@ const TableEditorPage: NextPageWithLayout = () => { const lastTab = tabStore.tabsMap[lastTabId] if (lastTab) router.push(`/project/${projectRef}/editor/${lastTab.metadata?.tableId}`) } + } else if (lastOpenedTab) { + router.push(`/project/${projectRef}/editor/${appSnap.dashboardHistory.editor}`) } }, [isTableEditorTabsEnabled]) diff --git a/apps/studio/pages/project/[ref]/functions/new.tsx b/apps/studio/pages/project/[ref]/functions/new.tsx index 26931123ec3..03d47e91046 100644 --- a/apps/studio/pages/project/[ref]/functions/new.tsx +++ b/apps/studio/pages/project/[ref]/functions/new.tsx @@ -281,7 +281,7 @@ const NewFunctionPage = () => { Templates - + diff --git a/apps/studio/sentry.client.config.ts b/apps/studio/sentry.client.config.ts index 44cc043b029..f107f699cb2 100644 --- a/apps/studio/sentry.client.config.ts +++ b/apps/studio/sentry.client.config.ts @@ -3,8 +3,8 @@ // https://docs.sentry.io/platforms/javascript/guides/nextjs/ import * as Sentry from '@sentry/nextjs' +import { hasConsented } from 'common' import { IS_PLATFORM } from 'common/constants/environment' -import { LOCAL_STORAGE_KEYS } from 'common/constants/local-storage' import { match } from 'path-to-regexp' // This is a workaround to ignore hCaptcha related errors. @@ -26,12 +26,9 @@ Sentry.init({ // Setting this option to true will print useful information to the console while you're setting up Sentry. debug: false, beforeSend(event, hint) { - const consent = - typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null + const consent = hasConsented() - if (IS_PLATFORM && consent === 'true') { + if (IS_PLATFORM && consent) { // Ignore invalid URL events for 99% of the time because it's using up a lot of quota. const isInvalidUrlEvent = (hint.originalException as any)?.message?.includes( `Failed to construct 'URL': Invalid URL` diff --git a/apps/studio/state/app-state.ts b/apps/studio/state/app-state.ts index 84438cfba36..186f4078c61 100644 --- a/apps/studio/state/app-state.ts +++ b/apps/studio/state/app-state.ts @@ -1,6 +1,5 @@ import { proxy, snapshot, subscribe, useSnapshot } from 'valtio' -import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS } from 'common' import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries' import { LOCAL_STORAGE_KEYS } from 'lib/constants' @@ -52,7 +51,6 @@ const getInitialState = () => { activeDocsSection: ['introduction'], docsLanguage: 'js', showProjectApiDocs: false, - isOptedInTelemetry: false, showEnableBranchingModal: false, showFeaturePreviewModal: false, selectedFeaturePreview: '', @@ -84,7 +82,6 @@ const getInitialState = () => { activeDocsSection: ['introduction'], docsLanguage: 'js', showProjectApiDocs: false, - isOptedInTelemetry: false, showEnableBranchingModal: false, showFeaturePreviewModal: false, selectedFeaturePreview: '', @@ -124,14 +121,6 @@ export const appState = proxy({ appState.docsLanguage = value }, - isOptedInTelemetry: false, - setIsOptedInTelemetry: (value: boolean | null) => { - appState.isOptedInTelemetry = value === null ? false : value - if (typeof window !== 'undefined' && value !== null) { - localStorage.setItem(COMMON_LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT, value.toString()) - } - }, - showEnableBranchingModal: false, setShowEnableBranchingModal: (value: boolean) => { appState.showEnableBranchingModal = value diff --git a/apps/studio/state/sql-editor-v2.ts b/apps/studio/state/sql-editor-v2.ts index 0f85816801b..94046904784 100644 --- a/apps/studio/state/sql-editor-v2.ts +++ b/apps/studio/state/sql-editor-v2.ts @@ -178,14 +178,15 @@ export const sqlEditorState = proxy({ saveFolder: ({ id, name }: { id: string; name: string }) => { let storeFolder = sqlEditorState.folders[id] + const isNewFolder = id === 'new-folder' const hasChanges = storeFolder.folder.name !== name - if (id === 'new-folder' && sqlEditorState.allFolderNames.includes(name)) { + if (isNewFolder && sqlEditorState.allFolderNames.includes(name)) { sqlEditorState.removeFolder(id) - return toast.error('This folder name already exists') + return toast.error('Unable to create new folder: This folder name already exists') } else if (hasChanges && sqlEditorState.allFolderNames.includes(name)) { storeFolder.status = 'idle' - return toast.error('This folder name already exists') + return toast.error('Unable to update folder: This folder name already exists') } const originalFolderName = storeFolder.folder.name.slice() diff --git a/apps/studio/state/storage-explorer.ts b/apps/studio/state/storage-explorer.ts deleted file mode 100644 index ae0a905b52e..00000000000 --- a/apps/studio/state/storage-explorer.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { proxy, snapshot, useSnapshot } from 'valtio' - -export const storageExplorerState = proxy({ - isSearching: false, - setIsSearching: (bool: boolean) => { - storageExplorerState.isSearching = bool - }, -}) - -export const getStorageExplorerStateSnapshot = () => snapshot(storageExplorerState) - -export const useStorageExplorerStateSnapshot = (options?: Parameters[1]) => - useSnapshot(storageExplorerState, options) diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx new file mode 100644 index 00000000000..886e8eeae19 --- /dev/null +++ b/apps/studio/state/storage-explorer.tsx @@ -0,0 +1,97 @@ +import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' +import { useLatest } from 'react-use' +import { proxy, snapshot, useSnapshot } from 'valtio' + +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { + STORAGE_SORT_BY, + STORAGE_SORT_BY_ORDER, + STORAGE_VIEWS, +} from 'components/to-be-cleaned/Storage/Storage.constants' +import { LOCAL_STORAGE_KEYS } from 'lib/constants' +import { tryParseJson } from 'lib/helpers' + +const DEFAULT_PREFERENCES = { + view: STORAGE_VIEWS.COLUMNS, + sortBy: STORAGE_SORT_BY.NAME, + sortByOrder: STORAGE_SORT_BY_ORDER.ASC, +} + +function createStorageExplorerState({ projectRef }: { projectRef: string }) { + const localStorageKey = LOCAL_STORAGE_KEYS.STORAGE_PREFERENCE(projectRef) + const { view, sortBy, sortByOrder } = + (typeof window !== 'undefined' && tryParseJson(localStorage?.getItem(localStorageKey))) || + DEFAULT_PREFERENCES + + const state = proxy({ + projectRef, + + view, + sortBy, + sortByOrder, + isSearching: false, + + setView: (value: STORAGE_VIEWS) => { + state.view = value + state.updateExplorerPreference() + }, + setSortBy: (value: STORAGE_SORT_BY) => { + state.sortBy = value + state.updateExplorerPreference() + }, + setSortByOrder: (value: STORAGE_SORT_BY_ORDER) => { + state.sortByOrder = value + state.updateExplorerPreference() + }, + setIsSearching: (value: boolean) => (state.isSearching = value), + + updateExplorerPreference: () => { + const localStorageKey = LOCAL_STORAGE_KEYS.STORAGE_PREFERENCE(projectRef) + const { view, sortBy, sortByOrder } = state + localStorage.setItem(localStorageKey, JSON.stringify({ view, sortBy, sortByOrder })) + }, + }) + + return state +} + +type StorageExplorerState = ReturnType + +const StorageExplorerStateContext = createContext( + createStorageExplorerState({ projectRef: '' }) +) + +export const StorageExplorerStateContextProvider = ({ children }: PropsWithChildren) => { + const { project } = useProjectContext() + const [state, setState] = useState(() => + createStorageExplorerState({ projectRef: project?.ref ?? '' }) + ) + + const stateRef = useLatest(state) + + // [Joshen] JFYI opting with the useEffect here as the storage explorer state was being loaded + // before the project details were ready, hence the store kept returning project ref as undefined + // Can be verified when we're saving the storage explorer preferences into local storage, that ref is undefined + // So the useEffect here is to make sure that the project ref is loaded into the state properly + // Although I'd be keen to re-investigate this to see if we can remove this + useEffect(() => { + const snap = snapshot(stateRef.current) + const hasDataReady = !!project?.ref + const isDifferentProject = snap.projectRef !== project?.ref + + if (hasDataReady && isDifferentProject) { + setState(createStorageExplorerState({ projectRef: project?.ref ?? '' })) + } + }, [project?.ref, stateRef]) + + return ( + + {children} + + ) +} + +export function useStorageExplorerStateSnapshot(options?: Parameters[1]) { + const state = useContext(StorageExplorerStateContext) + return useSnapshot(state, options) +} diff --git a/apps/studio/state/table-editor-table.tsx b/apps/studio/state/table-editor-table.tsx index ea62ab755d0..60c133eecea 100644 --- a/apps/studio/state/table-editor-table.tsx +++ b/apps/studio/state/table-editor-table.tsx @@ -17,14 +17,15 @@ import { useTableEditorStateSnapshot } from './table-editor' export const createTableEditorTableState = ({ projectRef, table: originalTable, - editable, + editable = true, onAddColumn, onExpandJSONEditor, onExpandTextEditor, }: { projectRef: string table: Entity - editable: boolean + /** If set to true, render an additional "+" column to support adding a new column in the grid editor */ + editable?: boolean onAddColumn: () => void onExpandJSONEditor: (column: string, row: SupaRow) => void onExpandTextEditor: (column: string, row: SupaRow) => void @@ -93,21 +94,29 @@ export const createTableEditorTableState = ({ state.gridColumns.splice(toIdx, 0, moveItem) }, updateColumnSize: (index: number, width: number) => { - ;(state.gridColumns[index] as CalculatedColumn & { width?: number }).width = width + if (state.gridColumns[index]) { + ;(state.gridColumns[index] as CalculatedColumn & { width?: number }).width = width + } }, freezeColumn: (columnKey: string) => { const index = state.gridColumns.findIndex((x) => x.key === columnKey) - ;(state.gridColumns[index] as CalculatedColumn & { frozen?: boolean }).frozen = true + if (state.gridColumns[index]) { + ;(state.gridColumns[index] as CalculatedColumn & { frozen?: boolean }).frozen = + true + } }, unfreezeColumn: (columnKey: string) => { const index = state.gridColumns.findIndex((x) => x.key === columnKey) - ;(state.gridColumns[index] as CalculatedColumn & { frozen?: boolean }).frozen = - false + if (state.gridColumns[index]) { + ;(state.gridColumns[index] as CalculatedColumn & { frozen?: boolean }).frozen = + false + } }, updateColumnIdx: (columnKey: string, columnIdx: number) => { const index = state.gridColumns.findIndex((x) => x.key === columnKey) - ;(state.gridColumns[index] as CalculatedColumn & { idx?: number }).idx = columnIdx - + if (state.gridColumns[index]) { + ;(state.gridColumns[index] as CalculatedColumn & { idx?: number }).idx = columnIdx + } state.gridColumns.sort((a, b) => a.idx - b.idx) }, diff --git a/apps/studio/state/tabs.ts b/apps/studio/state/tabs.ts index 7ddd4ea72ce..c04c828116a 100644 --- a/apps/studio/state/tabs.ts +++ b/apps/studio/state/tabs.ts @@ -291,29 +291,26 @@ export const handleTabClose = ({ if (!ref) return const store = getTabsStore(ref) - // tabs without the one we're closing - const currentTab = store.tabsMap[id] - const currentTabs = Object.values(store.tabsMap).filter((tab) => tab.id !== id) + const tabBeingClosed = store.tabsMap[id] + const tabsAfterClosing = Object.values(store.tabsMap).filter((tab) => tab.id !== id) const nextTabId = !editor ? undefined - : currentTabs.filter((tab) => { + : tabsAfterClosing.filter((tab) => { return editorEntityTypes[editor]?.includes(tab.type) })[0]?.id - delete store.tabsMap[id] - if (currentTab) { - // Update store - // If the tab being removed is logged in the store, update the open tabs - store.openTabs = [...currentTabs.map((tab) => tab.id).filter((id) => id !== currentTab.id)] + const { [id]: value, ...otherTabs } = store.tabsMap + store.tabsMap = otherTabs + + if (tabBeingClosed) { + const updatedOpenTabs = [...store.openTabs].filter((x) => x !== id) + store.openTabs = updatedOpenTabs } - // Check if there is a preview tab and if it matches the tab being closed - if (store.previewTabId) { - if (store.previewTabId === id) { - // remove the preview tab if it matches the tab being closed - store.previewTabId = undefined - } + // Remove the preview tab if it matches the tab being closed + if (store.previewTabId === id) { + store.previewTabId = undefined } // [Joshen] Only navigate away if we're closing the tab that's currently in focus @@ -325,7 +322,7 @@ export const handleTabClose = ({ onClearDashboardHistory() // If no tabs of same type, go to the home of the current section - switch (currentTab?.type) { + switch (tabBeingClosed?.type) { case 'sql': router.push(`/project/${router.query.ref}/sql`) break diff --git a/apps/studio/tests/components/Billing/TaxID.utils.test.ts b/apps/studio/tests/components/Billing/TaxID.utils.test.ts index 68908eddbf1..40d2960b0b8 100644 --- a/apps/studio/tests/components/Billing/TaxID.utils.test.ts +++ b/apps/studio/tests/components/Billing/TaxID.utils.test.ts @@ -1,4 +1,4 @@ -import { sanitizeTaxIdValue } from 'components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils' +import { sanitizeTaxIdValue } from 'components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.utils' import { describe, test, expect } from 'vitest' /** diff --git a/apps/studio/tests/pages/projects/Logs.Datepickers.test.tsx b/apps/studio/tests/pages/projects/Logs.Datepickers.test.tsx index c4ea241af56..82ecd7f9384 100644 --- a/apps/studio/tests/pages/projects/Logs.Datepickers.test.tsx +++ b/apps/studio/tests/pages/projects/Logs.Datepickers.test.tsx @@ -2,12 +2,12 @@ import { screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { LogsDatePicker } from 'components/interfaces/Settings/Logs/Logs.DatePickers' import { PREVIEWER_DATEPICKER_HELPERS } from 'components/interfaces/Settings/Logs/Logs.constants' +import { DatetimeHelper } from 'components/interfaces/Settings/Logs/Logs.types' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' import { expect, test, vi } from 'vitest' import { render } from '../../helpers' -import { DatetimeHelper } from 'components/interfaces/Settings/Logs/Logs.types' dayjs.extend(timezone) dayjs.extend(utc) @@ -94,17 +94,16 @@ test('datepicker onSubmit will return ISO string of selected dates', async () => // open the datepicker userEvent.click(screen.getByText(/13:00/i)) - // Get today's date and tomorrow's date - const today = dayjs() - const tomorrow = today.add(1, 'day') + const day15 = dayjs().date(15) + const day16 = day15.add(1, 'day') - // Find and click on today's date - const todayElement = await screen.findByText(today.format('D')) - userEvent.click(todayElement) + // Find and click on first date + const day15Element = await screen.findByText(day15.format('D')) + userEvent.click(day15Element) - // Find and click on tomorrow's date - const tomorrowElement = await screen.findByText(tomorrow.format('D')) - userEvent.click(tomorrowElement) + // Find and click on second date + const day16Element = await screen.findByText(day16.format('D')) + userEvent.click(day16Element) await userEvent.click(await screen.findByText('Apply')) expect(mockFn).toBeCalled() @@ -112,8 +111,8 @@ test('datepicker onSubmit will return ISO string of selected dates', async () => const call = mockFn.mock.calls[0][0] expect(call).toMatchObject({ - from: dayjs().date(today.date()).hour(13).minute(0).second(0).millisecond(0).toISOString(), - to: dayjs().date(tomorrow.date()).hour(23).minute(59).second(59).millisecond(0).toISOString(), + from: dayjs().date(day15.date()).hour(13).minute(0).second(0).millisecond(0).toISOString(), + to: dayjs().date(day16.date()).hour(23).minute(59).second(59).millisecond(0).toISOString(), }) }) diff --git a/apps/ui-library/app/(app)/page.tsx b/apps/ui-library/app/(app)/page.tsx index 129b8b3352c..dc0579832ae 100644 --- a/apps/ui-library/app/(app)/page.tsx +++ b/apps/ui-library/app/(app)/page.tsx @@ -41,8 +41,8 @@ export default function Home() { UI Blocks for Supabase Projects

- A collection of components and blocks built on the shadcn/ui library that - connect your front-end to your Supabase back-end via a single command. + A collection of React components and blocks built on the shadcn/ui library + that connect your front-end to your Supabase back-end via a single command.

Get Started diff --git a/apps/ui-library/app/(app)/telemetry-wrapper.tsx b/apps/ui-library/app/(app)/telemetry-wrapper.tsx index 5460e46156b..124d9c5477a 100644 --- a/apps/ui-library/app/(app)/telemetry-wrapper.tsx +++ b/apps/ui-library/app/(app)/telemetry-wrapper.tsx @@ -2,10 +2,10 @@ import { API_URL } from '@/lib/constants' import { IS_PLATFORM, PageTelemetry } from 'common' -import { useConsent } from 'ui-patterns/ConsentToast' +import { useConsentToast } from 'ui-patterns/consent' export const TelemetryWrapper = () => { - const { hasAcceptedConsent } = useConsent() + const { hasAcceptedConsent } = useConsentToast() return ( - - {children} - - + + + {children} + + + ) diff --git a/apps/ui-library/content/docs/getting-started/introduction.mdx b/apps/ui-library/content/docs/getting-started/introduction.mdx index 9747238e25c..bc2cd067f34 100644 --- a/apps/ui-library/content/docs/getting-started/introduction.mdx +++ b/apps/ui-library/content/docs/getting-started/introduction.mdx @@ -1,6 +1,6 @@ --- title: Introduction -description: A flexible, open-source UI component library built on shadcn/ui, designed to simplify Supabase-powered projects with pre-built Auth, Storage, and Realtime features. +description: A flexible, open-source, React-based UI component library built on shadcn/ui, designed to simplify Supabase-powered projects with pre-built Auth, Storage, and Realtime features. --- Our Supabase UI component library is a set of components built on top of the shadcn/ui registry system. It's designed to make building new or existing Supabase-powered projects faster and easier by providing pre-built solutions for common but challenging features—such as authentication, file uploads, and real-time updates. diff --git a/apps/ui-library/package.json b/apps/ui-library/package.json index 8157c99e4e7..8523c160c0f 100644 --- a/apps/ui-library/package.json +++ b/apps/ui-library/package.json @@ -70,7 +70,7 @@ "mdast-util-toc": "^6.1.1", "postcss": "^8", "react-dropzone": "^14.3.8", - "react-router": "^7.4.0", + "react-router": "^7.5.2", "rimraf": "^4.1.3", "shadcn": "2.4.0-canary.16", "shiki": "^1.1.7", diff --git a/apps/ui-library/public/img/supabase-og-image.png b/apps/ui-library/public/img/supabase-og-image.png index c2f4d0ea806..cba27d69859 100644 Binary files a/apps/ui-library/public/img/supabase-og-image.png and b/apps/ui-library/public/img/supabase-og-image.png differ diff --git a/apps/www/_customers/bree.mdx b/apps/www/_customers/bree.mdx index 411ea8e9e03..8e70581f263 100644 --- a/apps/www/_customers/bree.mdx +++ b/apps/www/_customers/bree.mdx @@ -30,7 +30,7 @@ region: 'North America' supabase_products: ['database', 'auth', 'functions', 'vector'] --- - + We hit scalability limits with Fauna, and Supabase was a step up across the board. The developer experience, ecosystem, support for AI. It’s the complete package. @@ -41,7 +41,7 @@ supabase_products: ['database', 'auth', 'functions', 'vector'] Bree had originally built their infrastructure on Fauna, a NoSQL database with a proprietary query language (FQL). The limitations quickly surfaced. - + Fauna’s custom language, FQL, introduced friction in our development process. It took time to learn and made it harder for our team to iterate and ship at the speed we needed. @@ -54,7 +54,7 @@ And when Fauna announced its end of life, it only added urgency. Bree needed a r Bree had long been aware of Supabase. Supabase had earned its reputation among builders and indie developers for its ease of use and instant setup. But what sold Bree was how well Supabase aligned with their long-term technical goals. - + Builders around the world use Supabase for exciting projects. It is so intuitive that developers can start building immediately and ship production-ready features in a day. @@ -65,7 +65,7 @@ After a thorough evaluation of vendors, Bree chose Supabase for several reasons: - A tightly integrated suite of tools: [Auth](/auth), [Edge Functions](/edge-functions), [Vectors](/modules/vector), and more. - A rich ecosystems of partners and plugins - + It’s almost the default database for AI-first development. It’s integrated with Lovable, with V0, and with Cursor—which we use heavily. Supabase just fits. @@ -94,14 +94,14 @@ Phase two, now underway, is focused on modularizing the data model, splitting la The results of Bree’s migration to Supabase have been immediate and measurable. Engineers can ship faster, performance is up across the board, and Supabase has become a foundational part of the team’s AI-driven development workflow. - + After migrating to Supabase, we saw immediate lift in developer velocity. Engineers can iterate on features much faster by writing Postgres SQL queries. Latency improvements are notable. Bree has observed double-digit reductions in response times across core functions and expects further gains as their architecture is optimized for Supabase. - + It already has double-digit latency improvements. With further optimization, some functions were 10x faster. @@ -117,7 +117,7 @@ Bree’s journey reflects the reasons developers and fast-moving teams choose Su - **AI-native infrastructure** – Supabase supports tools like Cursor and `pgvector` out of the box, making it ideal for AI-powered development - **Built to scale** – With high availability, backups, and a flexible Postgres foundation, Supabase is ready for growth - + Supabase puts your mind at ease. It has the best support experience I’ve ever seen. It’s a disruptive technology to bring to any organization. It’s very worth switching to. diff --git a/apps/www/_customers/euka.mdx b/apps/www/_customers/euka.mdx new file mode 100644 index 00000000000..7a8f2b4728a --- /dev/null +++ b/apps/www/_customers/euka.mdx @@ -0,0 +1,112 @@ +--- +name: Euka +title: 'Euka used Supabase to unlock faster growth' +# Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': +# meta_title: +description: Supabase helped Euka accelerate development, simplify AI feature rollout, and scale creator marketing faster than ever. A Postgres-based platform gave them the structure, speed, and flexibility they needed to grow. +# Use meta_description to add a custom meta description. Otherwise it defaults to {description}: +meta_description: Discover how Supabase helped Euka accelerate development. +author: prashant +author_title: Prashant Sridharan +author_url: https://github.com/CoolAssPuppy +author_image_url: https://avatars.githubusercontent.com/u/914007?v=4 +logo: /images/customers/logos/euka.png +logo_inverse: /images/customers/logos/light/euka.png +og_image: /images/customers/og/euka.jpg +tags: + - supabase +date: '2025-04-29' +company_url: https://www.euka.ai +stats: [{ stat: '00,000', label: Example stat }] +misc: [{ label: 'Founded', text: 'Canada' }] +about: Turning TikTok affiliates into a revenue-generating engine. +# "healthcare" | "fintech" | "ecommerce" | "education" | "gaming" | "media" | "real-estate" | "saas" | "social" | "analytics" | "ai" | "developer-tools" +industry: ['media', 'social'] +# "startup" | "enterprise" | "indie_dev" +company_size: 'startup' +# "Asia" | "Europe" | "North America" | "South America" | "Africa" | "Oceania" +region: 'North America' +# "database" | "auth" | "storage" | "realtime" | "functions" | "vector" +supabase_products: ['database', 'auth', 'storage', 'functions', 'vector'] +--- + + + Supabase gave us the tools we needed to move fast, scale quickly, and stay flexible as we grow — + all without the heavy lifting. + + +Euka is reimagining how brands scale their presence on social media. Their AI-powered platform helps businesses find, manage, and scale creator partnerships, primarily on TikTok Shop. By automating the most time-consuming parts of user-generated content (UGC) campaigns, Euka allows brands to build awareness and drive sales faster than ever before. + +As they expanded beyond ecommerce into SaaS and consumer software, Euka needed a backend platform that could scale with them without slowing them down. + +## The challenge + +Euka’s vision required a backend that could support rapid iteration, structured data analytics, seamless AI integration, and scalable storage. To succeed, Euka needed: + +- A relational database that supported structured queries. +- Integrated services like authentication, storage, and AI search. +- A platform that scaled easily as their business grew. +- A developer-first experience to enable faster shipping. + +## Choosing Supabase + +When it came time to choose a permanent foundation for the business, Kevin recalled a side project where Supabase had impressed him with its simplicity and speed. This time, for Euka, it was not just about familiarity. It was about finding the right platform to unlock their next phase of growth. + +Supabase provided everything they needed in one place: + +- A fully managed Postgres database with standard SQL support. +- Built-in authentication, storage, edge functions, and vector search. +- Native AI features like pgvector for semantic search. +- An intuitive dashboard and first-class developer experience. + + + It just worked. Supabase gave us a zero-to-one experience that let us build quickly without + worrying about the backend. + + +Supabase allowed the team to move faster without compromising on performance or scalability. It also future-proofed their stack for AI development without needing costly rewrites later. + +## How Euka built with Supabase + +Euka’s architecture focuses on simplicity and rapid iteration. They designed their system to get the most out of Supabase while maintaining flexibility for future growth. + +Their stack includes: + +- **Frontend**: Two Next.js apps deployed on Vercel, one for brands and one for creators. +- **Backend**: Python scripts for data scraping, trend analysis, and creator search, connected directly to Supabase. +- **Storage**: Supabase Storage for managing creator profile pictures, videos, and ecommerce product images. +- **Authentication**: Supabase Auth with ongoing fine-tuning around OAuth flows. +- **AI search**: Supabase’s `pgvector` extension for trending video discovery, using cosine similarity searches. + + + Setting up the database and storage was one of the easiest parts of building our MVP. + + +Supabase’s dashboard also became a powerful internal tool. Engineers could debug quickly by inspecting real-time logs and queries. Non-technical teams like marketing and sales used the dashboard to run simple queries, inspect user data, and export reports without needing custom-built admin panels. + +## The results + +Since launching on Supabase, Euka has accelerated every part of their development lifecycle. They can build quickly and are able to deliver new AI capabilities faster than expected. + + + With Supabase, we saw an immediate lift in developer velocity. Engineers can iterate on features + much faster by writing Postgres SQL queries. + + +Supabase helped Euka achieve: + +- Faster developer cycles by standardizing on SQL. +- Easier scaling through integrated, high-performance services. +- Simplified AI expansion using `pgvector` and seamless embedding storage. +- Greater internal collaboration through an intuitive, accessible dashboard. + +Supabase gave Euka a faster way to ship, a stronger foundation for AI growth, and a platform that can evolve alongside them as they expand into new industries. + + + Honestly, it’s a no-brainer these days. If you’re starting something new and need structured data, + Supabase is the obvious choice. + + +## Ready to build and scale with Supabase? + +Start your journey today at [www.supabase.com](/) diff --git a/apps/www/components/Hero/Hero.tsx b/apps/www/components/Hero/Hero.tsx index 80cd1a11cf0..303015cfa2b 100644 --- a/apps/www/components/Hero/Hero.tsx +++ b/apps/www/components/Hero/Hero.tsx @@ -16,18 +16,6 @@ const Hero = () => {
-
- -

Build in a weekend Scale to millions diff --git a/apps/www/components/logos.tsx b/apps/www/components/logos.tsx index 12f0e8b9440..b2ef09c79c7 100644 --- a/apps/www/components/logos.tsx +++ b/apps/www/components/logos.tsx @@ -78,11 +78,6 @@ const logos = [ alt: 'humata', name: 'humata', }, - { - image: `/images/logos/publicity/krea.svg`, - alt: 'krea', - name: 'krea', - }, { image: `/images/logos/publicity/udio.svg`, alt: 'udio', diff --git a/apps/www/data/CustomerStories.ts b/apps/www/data/CustomerStories.ts index cdca1608ba2..93350249549 100644 --- a/apps/www/data/CustomerStories.ts +++ b/apps/www/data/CustomerStories.ts @@ -18,6 +18,17 @@ export type CustomerStoryType = { } export const data: CustomerStoryType[] = [ + { + type: 'Customer Story', + title: 'Euka used Supabase to unlock faster growth', + description: + 'Supabase helped Euka accelerate development, simplify AI feature rollout, and scale creator marketing faster than ever.', + organization: 'Euka', + imgUrl: 'images/customers/logos/euka.png', + logo: '/images/customers/logos/euka.png', + logo_inverse: '/images/customers/logos/light/euka.png', + url: '/customers/euka', + }, { type: 'Customer Story', title: "Bree's Migration to Supabase from Fauna", diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx index f9b62cd6ea8..982d2721748 100644 --- a/apps/www/pages/_app.tsx +++ b/apps/www/pages/_app.tsx @@ -16,20 +16,19 @@ import Head from 'next/head' import { useRouter } from 'next/router' import { SonnerToaster, themes, TooltipProvider } from 'ui' import { CommandProvider } from 'ui-patterns/CommandMenu' -import { useConsent } from 'ui-patterns/ConsentToast' +import { useConsentToast } from 'ui-patterns/consent' import MetaFaviconsPagesRouter, { DEFAULT_FAVICON_ROUTE, DEFAULT_FAVICON_THEME_COLOR, } from 'common/MetaFavicons/pages-router' -import { LW14Announcement } from 'ui-patterns/Banners/LW14Announcement' import { WwwCommandMenu } from '~/components/CommandMenu' import { API_URL, APP_NAME, DEFAULT_META_DESCRIPTION } from '~/lib/constants' import useDarkLaunchWeeks from '../hooks/useDarkLaunchWeeks' export default function App({ Component, pageProps }: AppProps) { const router = useRouter() - const { hasAcceptedConsent } = useConsent() + const { hasAcceptedConsent } = useConsentToast() useThemeSandbox() @@ -95,7 +94,6 @@ export default function App({ Component, pageProps }: AppProps) { > - diff --git a/apps/www/public/customers-rss.xml b/apps/www/public/customers-rss.xml index fb6ea467bda..48b0f075937 100644 --- a/apps/www/public/customers-rss.xml +++ b/apps/www/public/customers-rss.xml @@ -5,9 +5,16 @@ https://supabase.com Latest news from Supabase en - Tue, 08 Apr 2025 00:00:00 -0700 + Tue, 29 Apr 2025 00:00:00 -0700 + https://supabase.com/customers/euka + Euka used Supabase to unlock faster growth + https://supabase.com/customers/euka + Supabase helped Euka accelerate development, simplify AI feature rollout, and scale creator marketing faster than ever. A Postgres-based platform gave them the structure, speed, and flexibility they needed to grow. + Tue, 29 Apr 2025 00:00:00 -0700 + + https://supabase.com/customers/bree Bree's Migration to Supabase from Fauna https://supabase.com/customers/bree diff --git a/apps/www/public/images/blog/avatars/kevin-wang-euka.jpeg b/apps/www/public/images/blog/avatars/kevin-wang-euka.jpeg new file mode 100644 index 00000000000..365a2020ba2 Binary files /dev/null and b/apps/www/public/images/blog/avatars/kevin-wang-euka.jpeg differ diff --git a/apps/www/public/images/customers/logos/euka.png b/apps/www/public/images/customers/logos/euka.png new file mode 100644 index 00000000000..d60da3dc2a6 Binary files /dev/null and b/apps/www/public/images/customers/logos/euka.png differ diff --git a/apps/www/public/images/customers/logos/light/euka.png b/apps/www/public/images/customers/logos/light/euka.png new file mode 100644 index 00000000000..6de3722bdb4 Binary files /dev/null and b/apps/www/public/images/customers/logos/light/euka.png differ diff --git a/apps/www/public/images/logos/publicity/krea.svg b/apps/www/public/images/logos/publicity/krea.svg deleted file mode 100644 index 76079718c47..00000000000 --- a/apps/www/public/images/logos/publicity/krea.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 54e2575df5e..5f0c9e04659 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -162,7 +162,7 @@ services: rest: container_name: supabase-rest - image: postgrest/postgrest:v12.2.10 + image: postgrest/postgrest:v12.2.11 restart: unless-stopped depends_on: db: @@ -231,7 +231,7 @@ services: # To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up storage: container_name: supabase-storage - image: supabase/storage-api:v1.22.3 + image: supabase/storage-api:v1.22.7 restart: unless-stopped volumes: - ./volumes/storage:/var/lib/storage:z diff --git a/examples/clerk/.gitignore b/examples/clerk/.gitignore new file mode 100644 index 00000000000..e2764f94943 --- /dev/null +++ b/examples/clerk/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# clerk configuration (can include secrets) +/.clerk/ diff --git a/examples/clerk/README.md b/examples/clerk/README.md new file mode 100644 index 00000000000..e215bc4ccf1 --- /dev/null +++ b/examples/clerk/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples/clerk/app/favicon.ico b/examples/clerk/app/favicon.ico new file mode 100644 index 00000000000..718d6fea483 Binary files /dev/null and b/examples/clerk/app/favicon.ico differ diff --git a/examples/clerk/app/globals.css b/examples/clerk/app/globals.css new file mode 100644 index 00000000000..a2dc41ecee5 --- /dev/null +++ b/examples/clerk/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/examples/clerk/app/layout.tsx b/examples/clerk/app/layout.tsx new file mode 100644 index 00000000000..4403e254775 --- /dev/null +++ b/examples/clerk/app/layout.tsx @@ -0,0 +1,55 @@ +import type { Metadata } from 'next' +import { + ClerkProvider, + OrganizationSwitcher, + SignInButton, + SignUpButton, + SignedIn, + SignedOut, + UserButton, +} from '@clerk/nextjs' +import { Geist, Geist_Mono } from 'next/font/google' +import './globals.css' +import { InsertData } from '@/components/insert' + +const geistSans = Geist({ + variable: '--font-geist-sans', + subsets: ['latin'], +}) + +const geistMono = Geist_Mono({ + variable: '--font-geist-mono', + subsets: ['latin'], +}) + +export const metadata: Metadata = { + title: 'Clerk Next.js Quickstart', + description: 'Generated by create next app', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + +
+ + + + + + + + + +
+ {children} + + +
+ ) +} diff --git a/examples/clerk/app/page.tsx b/examples/clerk/app/page.tsx new file mode 100644 index 00000000000..a2f1f35d279 --- /dev/null +++ b/examples/clerk/app/page.tsx @@ -0,0 +1,85 @@ +import { ReadData } from '@/components/read' +import Image from 'next/image' + +export default function Home() { + return ( +
+
+ Next.js logo +
    +
  1. + Get started by editing{' '} + + app/page.tsx + + . +
  2. +
  3. Save and see your changes instantly.
  4. +
+ + + +
+ +
+ ) +} diff --git a/examples/clerk/components/insert.tsx b/examples/clerk/components/insert.tsx new file mode 100644 index 00000000000..6e2f400d0c3 --- /dev/null +++ b/examples/clerk/components/insert.tsx @@ -0,0 +1,24 @@ +'use client' + +import { useSupabaseClient } from '@/hooks/useSupabaseClient' +import { useUser } from '@clerk/nextjs' + +export function InsertData() { + const supabase = useSupabaseClient() + + const { user } = useUser() + const organizationId = user?.organizationMemberships?.[0]?.organization?.id + + async function onInsertRow() { + if (organizationId) { + const { error } = await supabase + .from('secured_table') + .insert({ organization_id: organizationId }) + if (error) { + console.error(error) + } + } + } + + return +} diff --git a/examples/clerk/components/read.tsx b/examples/clerk/components/read.tsx new file mode 100644 index 00000000000..dc727c5ff80 --- /dev/null +++ b/examples/clerk/components/read.tsx @@ -0,0 +1,24 @@ +'use client' + +import { useSupabaseClient } from '@/hooks/useSupabaseClient' +import { useEffect, useState } from 'react' + +export function ReadData() { + const supabase = useSupabaseClient() + const [data, setData] = useState>([]) + + useEffect(() => { + supabase + .from('secured_table') + .select('*') + .then(({ data, error }) => { + if (error) { + console.error(error) + } else { + setData(data) + } + }) + }, [supabase]) + + return
{JSON.stringify(data, null, 2)}
+} diff --git a/examples/clerk/eslint.config.mjs b/examples/clerk/eslint.config.mjs new file mode 100644 index 00000000000..c85fb67c463 --- /dev/null +++ b/examples/clerk/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/examples/clerk/hooks/useSupabaseClient.ts b/examples/clerk/hooks/useSupabaseClient.ts new file mode 100644 index 00000000000..5c796d58e82 --- /dev/null +++ b/examples/clerk/hooks/useSupabaseClient.ts @@ -0,0 +1,16 @@ +import { useSession } from '@clerk/nextjs' +import { createClient } from '@supabase/supabase-js' + +export function useSupabaseClient() { + const { session } = useSession() + const supabaseClient = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + // Session accessed from Clerk SDK, either as Clerk.session (vanilla + // JavaScript) or useSession (React) + accessToken: async () => session?.getToken() ?? null, + } + ) + return supabaseClient +} diff --git a/examples/clerk/middleware.ts b/examples/clerk/middleware.ts new file mode 100644 index 00000000000..6f5aa9bd8f1 --- /dev/null +++ b/examples/clerk/middleware.ts @@ -0,0 +1,12 @@ +import { clerkMiddleware } from '@clerk/nextjs/server' + +export default clerkMiddleware() + +export const config = { + matcher: [ + // Skip Next.js internals and all static files, unless found in search params + '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', + // Always run for API routes + '/(api|trpc)(.*)', + ], +} diff --git a/examples/clerk/next.config.ts b/examples/clerk/next.config.ts new file mode 100644 index 00000000000..e9ffa3083ad --- /dev/null +++ b/examples/clerk/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/examples/clerk/package.json b/examples/clerk/package.json new file mode 100644 index 00000000000..5ca64fd2649 --- /dev/null +++ b/examples/clerk/package.json @@ -0,0 +1,29 @@ +{ + "name": "clerk", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@clerk/nextjs": "^6.18.2", + "@supabase/supabase-js": "^2.49.4", + "next": "15.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.3.1", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/examples/clerk/pnpm-lock.yaml b/examples/clerk/pnpm-lock.yaml new file mode 100644 index 00000000000..f1e977b279b --- /dev/null +++ b/examples/clerk/pnpm-lock.yaml @@ -0,0 +1,3906 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@clerk/nextjs': + specifier: ^6.18.2 + version: 6.18.2(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@supabase/supabase-js': + specifier: ^2.49.4 + version: 2.49.4 + next: + specifier: 15.3.1 + version: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: + specifier: ^19.0.0 + version: 19.1.0 + react-dom: + specifier: ^19.0.0 + version: 19.1.0(react@19.1.0) + devDependencies: + '@eslint/eslintrc': + specifier: ^3 + version: 3.3.1 + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.5 + '@types/node': + specifier: ^20 + version: 20.17.32 + '@types/react': + specifier: ^19 + version: 19.1.2 + '@types/react-dom': + specifier: ^19 + version: 19.1.3(@types/react@19.1.2) + eslint: + specifier: ^9 + version: 9.25.1(jiti@2.4.2) + eslint-config-next: + specifier: 15.3.1 + version: 15.3.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + tailwindcss: + specifier: ^4 + version: 4.1.5 + typescript: + specifier: ^5 + version: 5.8.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@clerk/backend@1.30.2': + resolution: {integrity: sha512-D8eCqiD6S0soqgJ9N0y0FC8MBvUX13d6GU9DgLIDOFltqzZ2QYP82Is7jzMBWb81+vqhx9kVp8EHPFyfI9buag==} + engines: {node: '>=18.17.0'} + peerDependencies: + svix: ^1.62.0 + peerDependenciesMeta: + svix: + optional: true + + '@clerk/clerk-react@5.30.2': + resolution: {integrity: sha512-sFFjNMIJG0Oy6ngO1bRQIUYhRfC32R2xtQMihhVd8bs97G5kFcGlwjpzxdwSw8NZKoQ6dONIu9lo4ACpWImFMw==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + + '@clerk/nextjs@6.18.2': + resolution: {integrity: sha512-COpljflwoX3uleIfrDNCllwyKaTIt3NTO02swm8ZKVA4opmHbQ4xdDg1RhTS+oMoK1jK7lSO50azkcil6kQNqA==} + engines: {node: '>=18.17.0'} + peerDependencies: + next: ^13.5.7 || ^14.2.25 || ^15.2.3 + react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + + '@clerk/shared@3.7.6': + resolution: {integrity: sha512-WB2LbNmdksYoPuQqbmEIeBBf/ncUAVtjn9prMH6KRcy5ira0Xu6ewmVy+DzOFKVyAKHM6iZcV3S5QMsk9wWriQ==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@clerk/types@4.56.1': + resolution: {integrity: sha512-PqPnAOJdjeNIlTgRGjEh5lAKPyoxuUyqGbMN8NazKJBqpnykuH9E/EXwTdROKDIvwmrV0L47U5sP2n6xu+4rbA==} + engines: {node: '>=18.17.0'} + + '@emnapi/core@1.4.3': + resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} + + '@emnapi/runtime@1.4.3': + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + + '@emnapi/wasi-threads@1.0.2': + resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} + + '@eslint-community/eslint-utils@4.6.1': + resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.20.0': + resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.2.1': + resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.13.0': + resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.25.1': + resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.8': + resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.2': + resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} + engines: {node: '>=18.18'} + + '@img/sharp-darwin-arm64@0.34.1': + resolution: {integrity: sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.1': + resolution: {integrity: sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.1.0': + resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.1.0': + resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.1.0': + resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.1.0': + resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.1.0': + resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.1.0': + resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.1.0': + resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.1': + resolution: {integrity: sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.1': + resolution: {integrity: sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.34.1': + resolution: {integrity: sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.1': + resolution: {integrity: sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.1': + resolution: {integrity: sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.1': + resolution: {integrity: sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.1': + resolution: {integrity: sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.34.1': + resolution: {integrity: sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.1': + resolution: {integrity: sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@napi-rs/wasm-runtime@0.2.9': + resolution: {integrity: sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==} + + '@next/env@15.3.1': + resolution: {integrity: sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==} + + '@next/eslint-plugin-next@15.3.1': + resolution: {integrity: sha512-oEs4dsfM6iyER3jTzMm4kDSbrQJq8wZw5fmT6fg2V3SMo+kgG+cShzLfEV20senZzv8VF+puNLheiGPlBGsv2A==} + + '@next/swc-darwin-arm64@15.3.1': + resolution: {integrity: sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.3.1': + resolution: {integrity: sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.3.1': + resolution: {integrity: sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@15.3.1': + resolution: {integrity: sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@15.3.1': + resolution: {integrity: sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@15.3.1': + resolution: {integrity: sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@15.3.1': + resolution: {integrity: sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.3.1': + resolution: {integrity: sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@rushstack/eslint-patch@1.11.0': + resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==} + + '@supabase/auth-js@2.69.1': + resolution: {integrity: sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==} + + '@supabase/functions-js@2.4.4': + resolution: {integrity: sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==} + + '@supabase/node-fetch@2.6.15': + resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==} + engines: {node: 4.x || >=6.0.0} + + '@supabase/postgrest-js@1.19.4': + resolution: {integrity: sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==} + + '@supabase/realtime-js@2.11.2': + resolution: {integrity: sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==} + + '@supabase/storage-js@2.7.1': + resolution: {integrity: sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==} + + '@supabase/supabase-js@2.49.4': + resolution: {integrity: sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==} + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.5': + resolution: {integrity: sha512-CBhSWo0vLnWhXIvpD0qsPephiaUYfHUX3U9anwDaHZAeuGpTiB3XmsxPAN6qX7bFhipyGBqOa1QYQVVhkOUGxg==} + + '@tailwindcss/oxide-android-arm64@4.1.5': + resolution: {integrity: sha512-LVvM0GirXHED02j7hSECm8l9GGJ1RfgpWCW+DRn5TvSaxVsv28gRtoL4aWKGnXqwvI3zu1GABeDNDVZeDPOQrw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.5': + resolution: {integrity: sha512-//TfCA3pNrgnw4rRJOqavW7XUk8gsg9ddi8cwcsWXp99tzdBAZW0WXrD8wDyNbqjW316Pk2hiN/NJx/KWHl8oA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.5': + resolution: {integrity: sha512-XQorp3Q6/WzRd9OalgHgaqgEbjP3qjHrlSUb5k1EuS1Z9NE9+BbzSORraO+ecW432cbCN7RVGGL/lSnHxcd+7Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.5': + resolution: {integrity: sha512-bPrLWbxo8gAo97ZmrCbOdtlz/Dkuy8NK97aFbVpkJ2nJ2Jo/rsCbu0TlGx8joCuA3q6vMWTSn01JY46iwG+clg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.5': + resolution: {integrity: sha512-1gtQJY9JzMAhgAfvd/ZaVOjh/Ju/nCoAsvOVJenWZfs05wb8zq+GOTnZALWGqKIYEtyNpCzvMk+ocGpxwdvaVg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.5': + resolution: {integrity: sha512-dtlaHU2v7MtdxBXoqhxwsWjav7oim7Whc6S9wq/i/uUMTWAzq/gijq1InSgn2yTnh43kR+SFvcSyEF0GCNu1PQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.5': + resolution: {integrity: sha512-fg0F6nAeYcJ3CriqDT1iVrqALMwD37+sLzXs8Rjy8Z1ZHshJoYceodfyUwGJEsQoTyWbliFNRs2wMQNXtT7MVA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.5': + resolution: {integrity: sha512-SO+F2YEIAHa1AITwc8oPwMOWhgorPzzcbhWEb+4oLi953h45FklDmM8dPSZ7hNHpIk9p/SCZKUYn35t5fjGtHA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.5': + resolution: {integrity: sha512-6UbBBplywkk/R+PqqioskUeXfKcBht3KU7juTi1UszJLx0KPXUo10v2Ok04iBJIaDPkIFkUOVboXms5Yxvaz+g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.5': + resolution: {integrity: sha512-hwALf2K9FHuiXTPqmo1KeOb83fTRNbe9r/Ixv9ZNQ/R24yw8Ge1HOWDDgTdtzntIaIUJG5dfXCf4g9AD4RiyhQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.5': + resolution: {integrity: sha512-oDKncffWzaovJbkuR7/OTNFRJQVdiw/n8HnzaCItrNQUeQgjy7oUiYpsm9HUBgpmvmDpSSbGaCa2Evzvk3eFmA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.5': + resolution: {integrity: sha512-WiR4dtyrFdbb+ov0LK+7XsFOsG+0xs0PKZKkt41KDn9jYpO7baE3bXiudPVkTqUEwNfiglCygQHl2jklvSBi7Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.5': + resolution: {integrity: sha512-1n4br1znquEvyW/QuqMKQZlBen+jxAbvyduU87RS8R3tUSvByAkcaMTkJepNIrTlYhD+U25K4iiCIxE6BGdRYA==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.5': + resolution: {integrity: sha512-5lAC2/pzuyfhsFgk6I58HcNy6vPK3dV/PoPxSDuOTVbDvCddYHzHiJZZInGIY0venvzzfrTEUAXJFULAfFmObg==} + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@20.17.32': + resolution: {integrity: sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==} + + '@types/phoenix@1.6.6': + resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} + + '@types/react-dom@19.1.3': + resolution: {integrity: sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.2': + resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@typescript-eslint/eslint-plugin@8.31.1': + resolution: {integrity: sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/parser@8.31.1': + resolution: {integrity: sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/scope-manager@8.31.1': + resolution: {integrity: sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.31.1': + resolution: {integrity: sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/types@8.31.1': + resolution: {integrity: sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.31.1': + resolution: {integrity: sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@8.31.1': + resolution: {integrity: sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/visitor-keys@8.31.1': + resolution: {integrity: sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-darwin-arm64@1.7.2': + resolution: {integrity: sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.7.2': + resolution: {integrity: sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.7.2': + resolution: {integrity: sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.7.2': + resolution: {integrity: sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.7.2': + resolution: {integrity: sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.7.2': + resolution: {integrity: sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.7.2': + resolution: {integrity: sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.7.2': + resolution: {integrity: sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.7.2': + resolution: {integrity: sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.7.2': + resolution: {integrity: sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.7.2': + resolution: {integrity: sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.7.2': + resolution: {integrity: sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.7.2': + resolution: {integrity: sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.7.2': + resolution: {integrity: sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.7.2': + resolution: {integrity: sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.7.2': + resolution: {integrity: sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.7.2': + resolution: {integrity: sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==} + cpu: [x64] + os: [win32] + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.10.3: + resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001716: + resolution: {integrity: sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + + es-abstract@1.23.9: + resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.1: + resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@15.3.1: + resolution: {integrity: sha512-GnmyVd9TE/Ihe3RrvcafFhXErErtr2jS0JDeCSp3vWvy86AXwHsRBt0E3MqP/m8ACS1ivcsi5uaqjbhsG18qKw==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.0: + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.31.0: + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.3.0: + resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.25.1: + resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.4.4: + resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.0: + resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-darwin-arm64@1.29.2: + resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.29.2: + resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.29.2: + resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.29.2: + resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.29.2: + resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.29.2: + resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.29.2: + resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.29.2: + resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.29.2: + resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.29.2: + resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.29.2: + resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.2.3: + resolution: {integrity: sha512-Mi7JISo/4Ij2tDZ2xBE2WH+/KvVlkhA6juEjpEeRAVPNCpN3nxJo/5FhDNKgBcdmcmhaH6JjgST4xY/23ZYK0w==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next@15.3.1: + resolution: {integrity: sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.34.1: + resolution: {integrity: sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + + snakecase-keys@8.0.1: + resolution: {integrity: sha512-Sj51kE1zC7zh6TDlNNz0/Jn1n5HiHdoQErxO8jLtnyrkJW/M5PrI7x05uDgY3BO7OUQYKCvmeMurW6BPUdwEOw==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + swr@2.3.3: + resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + tailwindcss@4.1.5: + resolution: {integrity: sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA==} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + tinyglobby@0.2.13: + resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@4.40.1: + resolution: {integrity: sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==} + engines: {node: '>=16'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + unrs-resolver@1.7.2: + resolution: {integrity: sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + ws@8.18.1: + resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@clerk/backend@1.30.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@clerk/shared': 3.7.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/types': 4.56.1 + cookie: 1.0.2 + snakecase-keys: 8.0.1 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-dom + + '@clerk/clerk-react@5.30.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@clerk/shared': 3.7.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/types': 4.56.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + + '@clerk/nextjs@6.18.2(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@clerk/backend': 1.30.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/clerk-react': 5.30.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/shared': 3.7.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/types': 4.56.1 + next: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + server-only: 0.0.1 + tslib: 2.8.1 + transitivePeerDependencies: + - svix + + '@clerk/shared@3.7.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@clerk/types': 4.56.1 + dequal: 2.0.3 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.5 + std-env: 3.9.0 + swr: 2.3.3(react@19.1.0) + optionalDependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@clerk/types@4.56.1': + dependencies: + csstype: 3.1.3 + + '@emnapi/core@1.4.3': + dependencies: + '@emnapi/wasi-threads': 1.0.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.6.1(eslint@9.25.1(jiti@2.4.2))': + dependencies: + eslint: 9.25.1(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.20.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.0 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.2.1': {} + + '@eslint/core@0.13.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.0 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.25.1': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.2.8': + dependencies: + '@eslint/core': 0.13.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.2': {} + + '@img/sharp-darwin-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.1.0 + optional: true + + '@img/sharp-darwin-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.1.0 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.1.0': + optional: true + + '@img/sharp-libvips-darwin-x64@1.1.0': + optional: true + + '@img/sharp-libvips-linux-arm64@1.1.0': + optional: true + + '@img/sharp-libvips-linux-arm@1.1.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.1.0': + optional: true + + '@img/sharp-libvips-linux-s390x@1.1.0': + optional: true + + '@img/sharp-libvips-linux-x64@1.1.0': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + optional: true + + '@img/sharp-linux-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.1.0 + optional: true + + '@img/sharp-linux-arm@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.1.0 + optional: true + + '@img/sharp-linux-s390x@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.1.0 + optional: true + + '@img/sharp-linux-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.1.0 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + optional: true + + '@img/sharp-wasm32@0.34.1': + dependencies: + '@emnapi/runtime': 1.4.3 + optional: true + + '@img/sharp-win32-ia32@0.34.1': + optional: true + + '@img/sharp-win32-x64@0.34.1': + optional: true + + '@napi-rs/wasm-runtime@0.2.9': + dependencies: + '@emnapi/core': 1.4.3 + '@emnapi/runtime': 1.4.3 + '@tybys/wasm-util': 0.9.0 + optional: true + + '@next/env@15.3.1': {} + + '@next/eslint-plugin-next@15.3.1': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@15.3.1': + optional: true + + '@next/swc-darwin-x64@15.3.1': + optional: true + + '@next/swc-linux-arm64-gnu@15.3.1': + optional: true + + '@next/swc-linux-arm64-musl@15.3.1': + optional: true + + '@next/swc-linux-x64-gnu@15.3.1': + optional: true + + '@next/swc-linux-x64-musl@15.3.1': + optional: true + + '@next/swc-win32-arm64-msvc@15.3.1': + optional: true + + '@next/swc-win32-x64-msvc@15.3.1': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@rtsao/scc@1.1.0': {} + + '@rushstack/eslint-patch@1.11.0': {} + + '@supabase/auth-js@2.69.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/functions-js@2.4.4': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/node-fetch@2.6.15': + dependencies: + whatwg-url: 5.0.0 + + '@supabase/postgrest-js@1.19.4': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/realtime-js@2.11.2': + dependencies: + '@supabase/node-fetch': 2.6.15 + '@types/phoenix': 1.6.6 + '@types/ws': 8.18.1 + ws: 8.18.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/storage-js@2.7.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/supabase-js@2.49.4': + dependencies: + '@supabase/auth-js': 2.69.1 + '@supabase/functions-js': 2.4.4 + '@supabase/node-fetch': 2.6.15 + '@supabase/postgrest-js': 1.19.4 + '@supabase/realtime-js': 2.11.2 + '@supabase/storage-js': 2.7.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.5': + dependencies: + enhanced-resolve: 5.18.1 + jiti: 2.4.2 + lightningcss: 1.29.2 + tailwindcss: 4.1.5 + + '@tailwindcss/oxide-android-arm64@4.1.5': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.5': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.5': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.5': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.5': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.5': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.5': + optional: true + + '@tailwindcss/oxide@4.1.5': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.5 + '@tailwindcss/oxide-darwin-arm64': 4.1.5 + '@tailwindcss/oxide-darwin-x64': 4.1.5 + '@tailwindcss/oxide-freebsd-x64': 4.1.5 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.5 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.5 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.5 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.5 + '@tailwindcss/oxide-linux-x64-musl': 4.1.5 + '@tailwindcss/oxide-wasm32-wasi': 4.1.5 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.5 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.5 + + '@tailwindcss/postcss@4.1.5': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.5 + '@tailwindcss/oxide': 4.1.5 + postcss: 8.5.3 + tailwindcss: 4.1.5 + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/estree@1.0.7': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@20.17.32': + dependencies: + undici-types: 6.19.8 + + '@types/phoenix@1.6.6': {} + + '@types/react-dom@19.1.3(@types/react@19.1.2)': + dependencies: + '@types/react': 19.1.2 + + '@types/react@19.1.2': + dependencies: + csstype: 3.1.3 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.17.32 + + '@typescript-eslint/eslint-plugin@8.31.1(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.31.1 + '@typescript-eslint/type-utils': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.31.1 + eslint: 9.25.1(jiti@2.4.2) + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.31.1 + '@typescript-eslint/types': 8.31.1 + '@typescript-eslint/typescript-estree': 8.31.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.31.1 + debug: 4.4.0 + eslint: 9.25.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.31.1': + dependencies: + '@typescript-eslint/types': 8.31.1 + '@typescript-eslint/visitor-keys': 8.31.1 + + '@typescript-eslint/type-utils@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.31.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + debug: 4.4.0 + eslint: 9.25.1(jiti@2.4.2) + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.31.1': {} + + '@typescript-eslint/typescript-estree@8.31.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.31.1 + '@typescript-eslint/visitor-keys': 8.31.1 + debug: 4.4.0 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.1 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.31.1 + '@typescript-eslint/types': 8.31.1 + '@typescript-eslint/typescript-estree': 8.31.1(typescript@5.8.3) + eslint: 9.25.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.31.1': + dependencies: + '@typescript-eslint/types': 8.31.1 + eslint-visitor-keys: 4.2.0 + + '@unrs/resolver-binding-darwin-arm64@1.7.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.7.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.7.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.7.2': + dependencies: + '@napi-rs/wasm-runtime': 0.2.9 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.7.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.7.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.7.2': + optional: true + + acorn-jsx@5.3.2(acorn@8.14.1): + dependencies: + acorn: 8.14.1 + + acorn@8.14.1: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.10.3: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001716: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + client-only@0.0.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + + concat-map@0.0.1: {} + + cookie@1.0.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + dequal@2.0.3: {} + + detect-libc@2.0.4: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + es-abstract@1.23.9: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + escape-string-regexp@4.0.0: {} + + eslint-config-next@15.3.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@next/eslint-plugin-next': 15.3.1 + '@rushstack/eslint-patch': 1.11.0 + '@typescript-eslint/eslint-plugin': 8.31.1(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.25.1(jiti@2.4.2) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.25.1(jiti@2.4.2)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.25.1(jiti@2.4.2)) + eslint-plugin-react: 7.37.5(eslint@9.25.1(jiti@2.4.2)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.25.1(jiti@2.4.2)) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.0 + eslint: 9.25.1(jiti@2.4.2) + get-tsconfig: 4.10.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.13 + unrs-resolver: 1.7.2 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.25.1(jiti@2.4.2)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.25.1(jiti@2.4.2)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.25.1(jiti@2.4.2) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.25.1(jiti@2.4.2)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.25.1(jiti@2.4.2) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.25.1(jiti@2.4.2)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.25.1(jiti@2.4.2)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.8 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.10.3 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.25.1(jiti@2.4.2) + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@5.2.0(eslint@9.25.1(jiti@2.4.2)): + dependencies: + eslint: 9.25.1(jiti@2.4.2) + + eslint-plugin-react@7.37.5(eslint@9.25.1(jiti@2.4.2)): + dependencies: + array-includes: 3.1.8 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 9.25.1(jiti@2.4.2) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.3.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.25.1(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.20.0 + '@eslint/config-helpers': 0.2.1 + '@eslint/core': 0.13.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.25.1 + '@eslint/plugin-kit': 0.2.8 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.2 + '@types/estree': 1.0.7 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.0 + escape-string-regexp: 4.0.0 + eslint-scope: 8.3.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) + eslint-visitor-keys: 4.2.0 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.4.4(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.10.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.3.2: + optional: true + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.1 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@2.4.2: {} + + js-cookie@3.0.5: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.8 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-darwin-arm64@1.29.2: + optional: true + + lightningcss-darwin-x64@1.29.2: + optional: true + + lightningcss-freebsd-x64@1.29.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.29.2: + optional: true + + lightningcss-linux-arm64-gnu@1.29.2: + optional: true + + lightningcss-linux-arm64-musl@1.29.2: + optional: true + + lightningcss-linux-x64-gnu@1.29.2: + optional: true + + lightningcss-linux-x64-musl@1.29.2: + optional: true + + lightningcss-win32-arm64-msvc@1.29.2: + optional: true + + lightningcss-win32-x64-msvc@1.29.2: + optional: true + + lightningcss@1.29.2: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.29.2 + lightningcss-darwin-x64: 1.29.2 + lightningcss-freebsd-x64: 1.29.2 + lightningcss-linux-arm-gnueabihf: 1.29.2 + lightningcss-linux-arm64-gnu: 1.29.2 + lightningcss-linux-arm64-musl: 1.29.2 + lightningcss-linux-x64-gnu: 1.29.2 + lightningcss-linux-x64-musl: 1.29.2 + lightningcss-win32-arm64-msvc: 1.29.2 + lightningcss-win32-x64-msvc: 1.29.2 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + map-obj@4.3.0: {} + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + napi-postinstall@0.2.3: {} + + natural-compare@1.4.0: {} + + next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@next/env': 15.3.1 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 + busboy: 1.6.0 + caniuse-lite: 1.0.30001716 + postcss: 8.4.31 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + styled-jsx: 5.1.6(react@19.1.0) + optionalDependencies: + '@next/swc-darwin-arm64': 15.3.1 + '@next/swc-darwin-x64': 15.3.1 + '@next/swc-linux-arm64-gnu': 15.3.1 + '@next/swc-linux-arm64-musl': 15.3.1 + '@next/swc-linux-x64-gnu': 15.3.1 + '@next/swc-linux-x64-musl': 15.3.1 + '@next/swc-win32-arm64-msvc': 15.3.1 + '@next/swc-win32-x64-msvc': 15.3.1 + sharp: 0.34.1 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.3: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + + react-is@16.13.1: {} + + react@19.1.0: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.26.0: {} + + semver@6.3.1: {} + + semver@7.7.1: {} + + server-only@0.0.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + sharp@0.34.1: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.1 + '@img/sharp-darwin-x64': 0.34.1 + '@img/sharp-libvips-darwin-arm64': 1.1.0 + '@img/sharp-libvips-darwin-x64': 1.1.0 + '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-linux-arm': 0.34.1 + '@img/sharp-linux-arm64': 0.34.1 + '@img/sharp-linux-s390x': 0.34.1 + '@img/sharp-linux-x64': 0.34.1 + '@img/sharp-linuxmusl-arm64': 0.34.1 + '@img/sharp-linuxmusl-x64': 0.34.1 + '@img/sharp-wasm32': 0.34.1 + '@img/sharp-win32-ia32': 0.34.1 + '@img/sharp-win32-x64': 0.34.1 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + snakecase-keys@8.0.1: + dependencies: + map-obj: 4.3.0 + snake-case: 3.0.4 + type-fest: 4.40.1 + + source-map-js@1.2.1: {} + + stable-hash@0.0.5: {} + + std-env@3.9.0: {} + + streamsearch@1.1.0: {} + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.23.9 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + styled-jsx@5.1.6(react@19.1.0): + dependencies: + client-only: 0.0.1 + react: 19.1.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + swr@2.3.3(react@19.1.0): + dependencies: + dequal: 2.0.3 + react: 19.1.0 + use-sync-external-store: 1.5.0(react@19.1.0) + + tailwindcss@4.1.5: {} + + tapable@2.2.1: {} + + tinyglobby@0.2.13: + dependencies: + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tr46@0.0.3: {} + + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@4.40.1: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.8.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.19.8: {} + + unrs-resolver@1.7.2: + dependencies: + napi-postinstall: 0.2.3 + optionalDependencies: + '@unrs/resolver-binding-darwin-arm64': 1.7.2 + '@unrs/resolver-binding-darwin-x64': 1.7.2 + '@unrs/resolver-binding-freebsd-x64': 1.7.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.7.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.7.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.7.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.7.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.7.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-x64-musl': 1.7.2 + '@unrs/resolver-binding-wasm32-wasi': 1.7.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.7.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.7.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.7.2 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-sync-external-store@1.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + ws@8.18.1: {} + + yocto-queue@0.1.0: {} diff --git a/examples/clerk/postcss.config.mjs b/examples/clerk/postcss.config.mjs new file mode 100644 index 00000000000..c7bcb4b1ee1 --- /dev/null +++ b/examples/clerk/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/examples/clerk/public/file.svg b/examples/clerk/public/file.svg new file mode 100644 index 00000000000..004145cddf3 --- /dev/null +++ b/examples/clerk/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/clerk/public/globe.svg b/examples/clerk/public/globe.svg new file mode 100644 index 00000000000..567f17b0d7c --- /dev/null +++ b/examples/clerk/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/clerk/public/next.svg b/examples/clerk/public/next.svg new file mode 100644 index 00000000000..5174b28c565 --- /dev/null +++ b/examples/clerk/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/clerk/public/vercel.svg b/examples/clerk/public/vercel.svg new file mode 100644 index 00000000000..77053960334 --- /dev/null +++ b/examples/clerk/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/clerk/public/window.svg b/examples/clerk/public/window.svg new file mode 100644 index 00000000000..b2b2a44f6eb --- /dev/null +++ b/examples/clerk/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/clerk/supabase/.gitignore b/examples/clerk/supabase/.gitignore new file mode 100644 index 00000000000..a3ad88055b7 --- /dev/null +++ b/examples/clerk/supabase/.gitignore @@ -0,0 +1,4 @@ +# Supabase +.branches +.temp +.env diff --git a/examples/clerk/supabase/config.toml b/examples/clerk/supabase/config.toml new file mode 100644 index 00000000000..ff6288b4ba3 --- /dev/null +++ b/examples/clerk/supabase/config.toml @@ -0,0 +1,161 @@ +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "clerk" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. public and storage are always included. +schemas = ["public", "storage", "graphql_public"] +# Extra schemas to add to the search_path of every request. public is always included. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 15 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv6) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = true +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }} ." + +# Use pre-defined map of phone number to OTP for testing. +[auth.sms.test_otp] +# 4152127777 = "123456" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +[auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" + +[analytics] +enabled = false +port = 54327 +vector_port = 54328 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/examples/clerk/supabase/migrations/20250501155648_setup_database.sql b/examples/clerk/supabase/migrations/20250501155648_setup_database.sql new file mode 100644 index 00000000000..535224a8ae4 --- /dev/null +++ b/examples/clerk/supabase/migrations/20250501155648_setup_database.sql @@ -0,0 +1,35 @@ +create table secured_table( + id uuid primary key default uuid_generate_v4(), + created_at timestamp with time zone default now(), + organization_id text not null +); + +alter table secured_table +enable row level security; + +create policy "Only organization admins can insert in table" +on secured_table +for insert +to authenticated +with check ( + (((select auth.jwt()->>'org_role') = 'org:admin') or ((select auth.jwt()->'o'->>'rol') = 'admin')) + and + (organization_id = (select coalesce(auth.jwt()->>'org_id', auth.jwt()->'o'->>'id'))) +); + +create policy "Users can select from their own organization" +on secured_table +for select +to authenticated +using ( + (organization_id = (select coalesce(auth.jwt()->>'org_id', auth.jwt()->'o'->>'id'))) +); + +create policy "Only users that have passed second factor verification can read from table" +on secured_table +as restrictive +for select +to authenticated +using ( + ((select auth.jwt()->'fva'->>1) != '-1') +); diff --git a/examples/clerk/supabase/seed.sql b/examples/clerk/supabase/seed.sql new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/clerk/tsconfig.json b/examples/clerk/tsconfig.json new file mode 100644 index 00000000000..d8b93235f20 --- /dev/null +++ b/examples/clerk/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/slack-clone/nextjs-slack-clone-dotenvx/README.md b/examples/slack-clone/nextjs-slack-clone-dotenvx/README.md index 794ed86eafe..15366afc747 100644 --- a/examples/slack-clone/nextjs-slack-clone-dotenvx/README.md +++ b/examples/slack-clone/nextjs-slack-clone-dotenvx/README.md @@ -137,7 +137,7 @@ SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS=https://.vercel.app/** Encrypt GitHub credentials in dotenv file: ```bash -npx dotenvx set SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET "" -f supabase/.env.production +npx @dotenvx/dotenvx set SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET "" -f supabase/.env.production ``` This also creates the encryption key in `supabase/.env.production` and the decryption key in `supabase/.env.keys`. @@ -145,9 +145,9 @@ This also creates the encryption key in `supabase/.env.production` and the decry 3. **Deploy to Supabase Remote:** ```bash -npx dotenvx run -f supabase/.env.production -- npx supabase link -npx dotenvx run -f supabase/.env.production -- npx supabase db push -npx dotenvx run -f supabase/.env.production -- npx supabase config push +npx @dotenvx/dotenvx run -f supabase/.env.production -- npx supabase link +npx @dotenvx/dotenvx run -f supabase/.env.production -- npx supabase db push +npx @dotenvx/dotenvx run -f supabase/.env.production -- npx supabase config push ``` ### How to Use with Preview Branches @@ -159,7 +159,7 @@ Here's how to set up encrypted secrets for your preview branches: 1. **Generate Key Pair and Encrypt Your Secrets:** ```bash -npx dotenvx set SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET "" -f supabase/.env.preview +npx @dotenvx/dotenvx set SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET "" -f supabase/.env.preview ``` This creates a new encryption key in `supabase/.env.preview` and a new decryption key in `supabase/.env.keys`, specifically for your preview branches. diff --git a/package.json b/package.json index e9d85ca7e2d..6bb693c1850 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "vinxi>esbuild": "0.25.2", "@tanstack/directive-functions-plugin>vite": "6.2.6", "@tanstack/react-start-plugin>vite": "6.2.6" - } + }, + "neverBuiltDependencies": ["libpg-query"] }, "engines": { "pnpm": ">=9", diff --git a/packages/ai-commands/package.json b/packages/ai-commands/package.json index 0a00ce7590e..fdbc75ef0db 100644 --- a/packages/ai-commands/package.json +++ b/packages/ai-commands/package.json @@ -25,6 +25,7 @@ "devDependencies": { "@ai-sdk/openai": "^0.0.72", "@types/common-tags": "^1.8.4", + "@types/mdast": "^4.0.0", "@types/node": "catalog:", "api-types": "workspace:*", "chalk": "^5.3.0", diff --git a/packages/ai-commands/test/sql-util.ts b/packages/ai-commands/test/sql-util.ts index d9c5eea6c66..6338685d734 100644 --- a/packages/ai-commands/test/sql-util.ts +++ b/packages/ai-commands/test/sql-util.ts @@ -1,4 +1,4 @@ -import { A_Const, A_Expr, ColumnRef, CreatePolicyStmt, Node, parseQuery } from 'libpg-query' +import { A_Const, A_Expr, ColumnRef, CreatePolicyStmt, Node, parseQuery } from 'libpg-query/wasm' export type PolicyInfo = { name: string diff --git a/packages/ai-commands/test/util.ts b/packages/ai-commands/test/util.ts index 6a911fcc032..b73bba0834c 100644 --- a/packages/ai-commands/test/util.ts +++ b/packages/ai-commands/test/util.ts @@ -1,6 +1,6 @@ import chalk from 'chalk' +import type { Code } from 'mdast' import { fromMarkdown } from 'mdast-util-from-markdown' -import type { Code } from 'mdast-util-from-markdown/lib' import { format } from 'sql-formatter' declare global { diff --git a/packages/ai-commands/tsconfig.json b/packages/ai-commands/tsconfig.json index f612f0c8a27..825ae2a5a01 100644 --- a/packages/ai-commands/tsconfig.json +++ b/packages/ai-commands/tsconfig.json @@ -3,6 +3,7 @@ "include": ["."], "exclude": ["dist", "build", "node_modules"], "compilerOptions": { + "moduleResolution": "bundler", "types": ["./test/extensions.d.ts"] } } diff --git a/packages/ai-commands/vitest.setup.ts b/packages/ai-commands/vitest.setup.ts index 1b83069042e..b8e0e2e20d8 100644 --- a/packages/ai-commands/vitest.setup.ts +++ b/packages/ai-commands/vitest.setup.ts @@ -9,3 +9,27 @@ if (!process.env.CI) { statSync(envPath) config({ path: envPath }) } + +// Modify fetch to support wasm file URLs +globalThis.fetch = async (input: string | URL | Request, init?: RequestInit) => { + if (input instanceof Request) { + return fetch(input, init) + } + + const url = new URL(input) + if (url.protocol === 'file:' && url.pathname.endsWith('.wasm')) { + return fetchFileNode(input, 'application/wasm') + } + + return fetch(input, init) +} + +async function fetchFileNode(input: string | URL, type: string) { + const fs = await import('node:fs') + const { Readable } = await import('node:stream') + const { fileURLToPath } = await import('node:url') + const path = fileURLToPath(input) + const nodeStream = fs.createReadStream(path) + const stream = Readable.toWeb(nodeStream) as ReadableStream + return new Response(stream, { headers: { 'Content-Type': type } }) +} diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 4f4cd475400..661af01cdbb 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -237,6 +237,48 @@ export interface paths { patch?: never trace?: never } + '/v1/projects/{ref}/advisors/performance': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Gets project performance advisors. + * @deprecated + * @description This is an **experimental** endpoint. It is subject to change or removal in future versions. Use it with caution, as it may not remain supported or stable. + */ + get: operations['getPerformanceAdvisors'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v1/projects/{ref}/advisors/security': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Gets project security advisors. + * @deprecated + * @description This is an **experimental** endpoint. It is subject to change or removal in future versions. Use it with caution, as it may not remain supported or stable. + */ + get: operations['getSecurityAdvisors'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/v1/projects/{ref}/analytics/endpoints/logs.all': { parameters: { query?: never @@ -891,6 +933,23 @@ export interface paths { patch?: never trace?: never } + '/v1/projects/{ref}/network-bans/retrieve/enriched': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** [Beta] Gets project's network bans with additional information about which databases they affect */ + post: operations['v1-list-all-network-bans-enriched'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/v1/projects/{ref}/network-restrictions': { parameters: { query?: never @@ -1347,6 +1406,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' ) | 'cd_default' | ('pitr_7' | 'pitr_14' | 'pitr_28') @@ -1419,6 +1486,7 @@ export interface components { external_twitter_client_id: string | null external_twitter_enabled: boolean | null external_twitter_secret: string | null + external_web3_solana_enabled: boolean | null external_workos_client_id: string | null external_workos_enabled: boolean | null external_workos_secret: string | null @@ -1478,6 +1546,7 @@ export interface components { rate_limit_sms_sent: number | null rate_limit_token_refresh: number | null rate_limit_verify: number | null + rate_limit_web3: number | null refresh_token_rotation_enabled: boolean | null saml_allow_encrypted_assertions: boolean | null saml_enabled: boolean | null @@ -1604,7 +1673,7 @@ export interface components { status: 'ACTIVE' | 'REMOVED' | 'THROTTLED' verify_jwt?: boolean version: number - } + }[] BulkUpdateFunctionResponse: { functions: { created_at: number @@ -1645,6 +1714,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' git_branch?: string persistent?: boolean /** @@ -1719,7 +1796,7 @@ export interface components { */ name: string value: string - } + }[] CreateSigningKeyBody: { /** @enum {string} */ algorithm: 'EdDSA' | 'ES256' | 'RS256' | 'HS256' @@ -1909,6 +1986,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' ) | 'cd_default' | ('pitr_7' | 'pitr_14' | 'pitr_28') @@ -1952,6 +2037,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' ) | 'cd_default' | ('pitr_7' | 'pitr_14' | 'pitr_28') @@ -2005,6 +2098,13 @@ export interface components { NetworkBanResponse: { banned_ipv4_addresses: string[] } + NetworkBanResponseEnriched: { + banned_ipv4_addresses: { + banned_address: string + identifier: string + type: string + }[] + } NetworkRestrictionsRequest: { dbAllowedCidrs?: string[] dbAllowedCidrsV6?: string[] @@ -2132,12 +2232,12 @@ export interface components { /** @enum {string} */ ReleaseChannel: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' | 'preview' RemoveNetworkBanRequest: { + identifier?: string ipv4_addresses: string[] } RemoveReadReplicaBody: { database_identifier: string } - RestoreProjectBodyDto: Record SecretResponse: { name: string updated_at?: string @@ -2375,6 +2475,7 @@ export interface components { external_twitter_client_id?: string | null external_twitter_enabled?: boolean | null external_twitter_secret?: string | null + external_web3_solana_enabled?: boolean | null external_workos_client_id?: string | null external_workos_enabled?: boolean | null external_workos_secret?: string | null @@ -2440,6 +2541,7 @@ export interface components { rate_limit_sms_sent?: number | null rate_limit_token_refresh?: number | null rate_limit_verify?: number | null + rate_limit_web3?: number | null refresh_token_rotation_enabled?: boolean | null saml_enabled?: boolean | null saml_external_url?: string | null @@ -2680,12 +2782,6 @@ export interface components { region: string walg_enabled: boolean } - V1CreateFunctionBody: { - body: string - name: string - slug: string - verify_jwt?: boolean - } V1CreateMigrationBody: { name?: string query: string @@ -2705,6 +2801,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' /** * @deprecated * @description This field is deprecated and is ignored in this request @@ -2799,6 +2903,59 @@ export interface components { db_schema: string max_rows: number } + V1ProjectAdvisorsResponseDto: { + lints: { + cache_key: string + categories: ('PERFORMANCE' | 'SECURITY')[] + description: string + detail: string + /** @enum {string} */ + facing: 'EXTERNAL' + /** @enum {string} */ + level: 'ERROR' | 'WARN' | 'INFO' + metadata?: { + entity?: string + fkey_columns?: number[] + fkey_name?: string + name?: string + schema?: string + /** @enum {string} */ + type?: 'table' | 'view' | 'auth' | 'function' | 'extension' | 'compliance' + } + /** @enum {string} */ + name: + | 'unindexed_foreign_keys' + | 'auth_users_exposed' + | 'auth_rls_initplan' + | 'no_primary_key' + | 'unused_index' + | 'multiple_permissive_policies' + | 'policy_exists_rls_disabled' + | 'rls_enabled_no_policy' + | 'duplicate_index' + | 'security_definer_view' + | 'function_search_path_mutable' + | 'rls_disabled_in_public' + | 'extension_in_public' + | 'rls_references_user_metadata' + | 'materialized_view_in_api' + | 'foreign_table_in_api' + | 'unsupported_reg_types' + | 'auth_otp_long_expiry' + | 'auth_otp_short_length' + | 'ssl_not_enforced' + | 'network_restrictions_not_set' + | 'password_requirements_min_length' + | 'pitr_not_enabled' + | 'auth_leaked_password_protection' + | 'auth_insufficient_mfa_options' + | 'auth_password_policy_missing' + | 'leaked_service_key' + | 'no_backup_admin' + remediation: string + title: string + }[] + } V1ProjectRefResponse: { /** Format: int64 */ id: number @@ -2945,6 +3102,7 @@ export interface operations { query?: never header?: never path: { + /** @description Branch ID */ branch_id: string } cookie?: never @@ -2973,6 +3131,7 @@ export interface operations { query?: never header?: never path: { + /** @description Branch ID */ branch_id: string } cookie?: never @@ -3001,6 +3160,7 @@ export interface operations { query?: never header?: never path: { + /** @description Branch ID */ branch_id: string } cookie?: never @@ -3033,6 +3193,7 @@ export interface operations { query?: never header?: never path: { + /** @description Branch ID */ branch_id: string } cookie?: never @@ -3065,6 +3226,7 @@ export interface operations { query?: never header?: never path: { + /** @description Branch ID */ branch_id: string } cookie?: never @@ -3097,6 +3259,7 @@ export interface operations { query?: never header?: never path: { + /** @description Branch ID */ branch_id: string } cookie?: never @@ -3131,7 +3294,9 @@ export interface operations { code_challenge?: string code_challenge_method?: 'plain' | 'sha256' | 'S256' redirect_uri: string - response_type: string + response_mode?: string + response_type: 'code' | 'token' | 'id_token token' + scope?: string state?: string } header?: never @@ -3253,6 +3418,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -3280,6 +3446,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -3407,6 +3574,62 @@ export interface operations { } } } + getPerformanceAdvisors: { + parameters: { + query?: never + header?: never + path: { + /** @description Project ref */ + ref: string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['V1ProjectAdvisorsResponseDto'] + } + } + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } + getSecurityAdvisors: { + parameters: { + query?: never + header?: never + path: { + /** @description Project ref */ + ref: string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['V1ProjectAdvisorsResponseDto'] + } + } + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } getLogs: { parameters: { query?: { @@ -3416,6 +3639,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3441,10 +3665,12 @@ export interface operations { 'v1-get-project-api-keys': { parameters: { query?: { + /** @description Boolean string, true or false */ reveal?: boolean } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3470,10 +3696,12 @@ export interface operations { createApiKey: { parameters: { query?: { + /** @description Boolean string, true or false */ reveal?: boolean } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3503,11 +3731,13 @@ export interface operations { getApiKey: { parameters: { query?: { + /** @description Boolean string, true or false */ reveal?: boolean } header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -3533,11 +3763,13 @@ export interface operations { deleteApiKey: { parameters: { query?: { + /** @description Boolean string, true or false */ reveal?: boolean } header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -3563,11 +3795,13 @@ export interface operations { updateApiKey: { parameters: { query?: { + /** @description Boolean string, true or false */ reveal?: boolean } header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -3599,6 +3833,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3633,6 +3868,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3669,22 +3905,8 @@ export interface operations { query?: never header?: never path: { - addon_variant: - | 'ci_micro' - | 'ci_small' - | 'ci_medium' - | 'ci_large' - | 'ci_xlarge' - | 'ci_2xlarge' - | 'ci_4xlarge' - | 'ci_8xlarge' - | 'ci_12xlarge' - | 'ci_16xlarge' - | 'cd_default' - | 'pitr_7' - | 'pitr_14' - | 'pitr_28' - | 'ipv4_default' + addon_variant: unknown + /** @description Project ref */ ref: string } cookie?: never @@ -3717,6 +3939,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3751,6 +3974,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3789,6 +4013,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3821,6 +4046,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3855,6 +4081,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3893,6 +4120,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3920,6 +4148,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3953,6 +4182,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -3981,6 +4211,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -4009,6 +4240,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -4040,6 +4272,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4074,6 +4307,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4113,6 +4347,7 @@ export interface operations { header?: never path: { provider_id: string + /** @description Project ref */ ref: string } cookie?: never @@ -4148,6 +4383,7 @@ export interface operations { header?: never path: { provider_id: string + /** @description Project ref */ ref: string } cookie?: never @@ -4187,6 +4423,7 @@ export interface operations { header?: never path: { provider_id: string + /** @description Project ref */ ref: string } cookie?: never @@ -4221,6 +4458,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4248,6 +4486,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4279,6 +4518,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string tpa_id: string } @@ -4307,6 +4547,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string tpa_id: string } @@ -4370,6 +4611,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4398,6 +4640,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4436,6 +4679,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4470,6 +4714,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4508,6 +4753,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4542,6 +4788,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4578,6 +4825,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4612,6 +4860,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4644,6 +4893,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4678,6 +4928,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4716,6 +4967,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4785,6 +5037,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4814,6 +5067,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4841,6 +5095,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4873,8 +5128,12 @@ export interface operations { 'v1-apply-a-migration': { parameters: { query?: never - header?: never + header?: { + /** @description A unique key to ensure the same migration is tracked only once. */ + 'Idempotency-Key'?: string + } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4913,6 +5172,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4984,6 +5244,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5018,13 +5279,14 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never } requestBody: { content: { - 'application/json': components['schemas']['BulkUpdateFunctionBody'][] + 'application/json': components['schemas']['BulkUpdateFunctionBody'] } } responses: { @@ -5055,24 +5317,22 @@ export interface operations { parameters: { query?: { entrypoint_path?: string + /** @description Boolean string, true or false */ import_map?: boolean import_map_path?: string name?: string slug?: string + /** @description Boolean string, true or false */ verify_jwt?: boolean } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never } - requestBody: { - content: { - 'application/json': components['schemas']['V1CreateFunctionBody'] - 'application/vnd.denoland.eszip': components['schemas']['V1CreateFunctionBody'] - } - } + requestBody?: never responses: { 201: { headers: { @@ -5102,7 +5362,9 @@ export interface operations { query?: never header?: never path: { + /** @description Function slug */ function_slug: string + /** @description Project ref */ ref: string } cookie?: never @@ -5137,7 +5399,9 @@ export interface operations { query?: never header?: never path: { + /** @description Function slug */ function_slug: string + /** @description Project ref */ ref: string } cookie?: never @@ -5169,14 +5433,19 @@ export interface operations { parameters: { query?: { entrypoint_path?: string + /** @description Boolean string, true or false */ import_map?: boolean import_map_path?: string name?: string + slug?: string + /** @description Boolean string, true or false */ verify_jwt?: boolean } header?: never path: { + /** @description Function slug */ function_slug: string + /** @description Project ref */ ref: string } cookie?: never @@ -5216,7 +5485,9 @@ export interface operations { query?: never header?: never path: { + /** @description Function slug */ function_slug: string + /** @description Project ref */ ref: string } cookie?: never @@ -5247,11 +5518,13 @@ export interface operations { 'v1-deploy-a-function': { parameters: { query?: { + /** @description Boolean string, true or false */ bundleOnly?: boolean slug?: string } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5293,6 +5566,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5327,6 +5601,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5363,6 +5638,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5392,11 +5668,47 @@ export interface operations { } } } + 'v1-list-all-network-bans-enriched': { + parameters: { + query?: never + header?: never + path: { + /** @description Project ref */ + ref: string + } + cookie?: never + } + requestBody?: never + responses: { + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['NetworkBanResponseEnriched'] + } + } + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Failed to retrieve project's enriched network bans */ + 500: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } 'v1-get-network-restrictions': { parameters: { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5431,6 +5743,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5495,6 +5808,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5529,6 +5843,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5641,6 +5956,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5677,6 +5993,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5781,6 +6098,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5808,15 +6126,12 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never } - requestBody: { - content: { - 'application/json': components['schemas']['RestoreProjectBodyDto'] - } - } + requestBody?: never responses: { 200: { headers: { @@ -5837,6 +6152,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5862,6 +6178,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5896,13 +6213,14 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never } requestBody: { content: { - 'application/json': components['schemas']['CreateSecretBody'][] + 'application/json': components['schemas']['CreateSecretBody'] } } responses: { @@ -5932,6 +6250,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5970,6 +6289,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6004,6 +6324,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6079,6 +6400,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6113,6 +6435,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6151,6 +6474,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6187,6 +6511,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6221,6 +6546,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6255,6 +6581,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6287,6 +6614,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6325,6 +6653,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6363,6 +6692,7 @@ export interface operations { query?: { cursor?: string limit?: string + /** @description Project ref */ project_ref?: string sort_by?: 'name' | 'inserted_at' sort_order?: 'asc' | 'desc' diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index de596658974..4a330ce8f5b 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -1216,6 +1216,24 @@ export interface paths { patch?: never trace?: never } + '/platform/organizations/{slug}/members/mfa/enforcement': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Gets organization MFA enforcement state */ + get: operations['MfaController_getMFAState'] + put?: never + post?: never + delete?: never + options?: never + head?: never + /** Update organization MFA enforcement state */ + patch: operations['MfaController_updateMFAState'] + trace?: never + } '/platform/organizations/{slug}/members/reached-free-project-limit': { parameters: { query?: never @@ -2987,6 +3005,23 @@ export interface paths { patch?: never trace?: never } + '/platform/projects/{ref}/run-lints/{name}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Run project lint by name */ + get: operations['ProjectRunLintsController_runLintByName'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/platform/projects/{ref}/run-lints/leaked-service-key': { parameters: { query?: never @@ -3140,63 +3175,6 @@ export interface paths { patch?: never trace?: never } - '/platform/props/project/{ref}/api': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** - * Gets project's api info - * @deprecated - */ - get: operations['ApiController_getProjectApi'] - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/platform/props/project/{ref}/jwt-secret-update-status': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** Gets the last JWT secret update status */ - get: operations['JwtSecretUpdateStatusController_getJwtSecretUpdateStatus'] - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/platform/props/project/{ref}/settings': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** - * Gets project's settings - * @deprecated - */ - get: operations['PropsSettingsController_getProjectSettingsDeprecated'] - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } '/platform/replication/{ref}/pipelines': { parameters: { query?: never @@ -4060,34 +4038,6 @@ export interface components { } result?: unknown[] } - ApiResponse: { - autoApiService: { - app: { - id: number - name: string - } - app_config: { - [key: string]: unknown - } - defaultApiKey: string - endpoint: string - id: number - name: string - project: { - /** @description Project ref */ - ref: string - } - /** @enum {string} */ - protocol: 'https' - restUrl: string - service_api_keys: { - api_key_encrypted?: string - name: string - tags: string - }[] - serviceApiKey: string - } - } ApproveAuthorizationResponse: { url: string } @@ -4183,11 +4133,15 @@ export interface components { postal_code?: string state?: string } + billing_name?: string } Buffer: Record BulkDeleteUserContentResponse: { id: string } + ChangeMFAEnforcementStateRequestDto: { + enforced: boolean + } CloneBackupsResponse: { backups: { id: number @@ -4288,6 +4242,10 @@ export interface components { session_id: string token_name?: string } + CreateCollectionBody: { + name: string + retention_days: number + } CreateColumnBody: { check?: string comment?: string @@ -4469,10 +4427,24 @@ export interface components { id: string } CreateOrganizationBody: { + address?: { + city?: string + country: string + line1: string + line2?: string + postal_code?: string + state?: string + } + billing_name?: string kind?: string name: string payment_method?: string size?: string + tax_id?: { + country?: string + type: string + value: string + } /** @enum {string} */ tier: 'tier_free' | 'tier_pro' | 'tier_payg' | 'tier_team' | 'tier_enterprise' } @@ -4525,6 +4497,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' name: string org_id?: number organization_slug?: string @@ -4559,6 +4539,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' inserted_at: string | null is_branch_enabled: boolean is_physical_backups_enabled: boolean | null @@ -4783,6 +4771,7 @@ export interface components { state?: string } balance: number + billing_name?: string billing_via_partner: boolean email: string } @@ -4856,7 +4845,6 @@ export interface components { } DeleteOAuthAppResponse: { client_id: string - client_secret_alias: string created_at: string icon?: string id: string @@ -5269,6 +5257,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' | 'cd_default' | 'pitr_7' | 'pitr_14' @@ -5535,6 +5531,7 @@ export interface components { RATE_LIMIT_SMS_SENT: number RATE_LIMIT_TOKEN_REFRESH: number RATE_LIMIT_VERIFY: number + RATE_LIMIT_WEB3?: number | null REFRESH_TOKEN_ROTATION_ENABLED: boolean SAML_ALLOW_ENCRYPTED_ASSERTIONS: boolean SAML_ENABLED: boolean @@ -5588,9 +5585,6 @@ export interface components { /** @enum {string} */ privilege_type: 'ALL' | 'SELECT' | 'INSERT' | 'UPDATE' | 'REFERENCES' }[] - HCaptchaBody: { - hcaptchaToken: string - } InvitationByTokenResponse: { authorized_user: boolean email_match: boolean @@ -5622,17 +5616,6 @@ export interface components { InvoicePaymentLinkResponse: { redirectUrl: string } - JwtSecretUpdateStatus: { - jwtSecretUpdateStatus: { - change_tracking_id?: string - /** @enum {number} */ - error?: 0 | 1 | 2 | 3 | 4 | 5 - /** @enum {number} */ - progress?: 0 | 1 | 2 | 3 | 4 | 5 - /** @enum {number} */ - status?: 0 | 1 | 2 - } - } LFAccessToken: { description: string | null id: number @@ -5822,6 +5805,9 @@ export interface components { primary_email: string username: string } + MfaStatusResponseDto: { + enforced: boolean + } MoveObjectBody: { from: string to: string @@ -5859,7 +5845,6 @@ export interface components { app_id?: string authorized_at?: string client_id?: string - client_secret_alias?: string created_at?: string created_by?: string icon?: string @@ -5916,6 +5901,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' region: string /** @enum {string} */ status: @@ -6020,6 +6013,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' inserted_at: string | null is_branch_enabled: boolean is_physical_backups_enabled: boolean | null @@ -6090,6 +6091,14 @@ export interface components { | 'COMPUTE_HOURS_8XL' | 'COMPUTE_HOURS_12XL' | 'COMPUTE_HOURS_16XL' + | 'COMPUTE_HOURS_24XL' + | 'COMPUTE_HOURS_24XL_OPTIMIZED_CPU' + | 'COMPUTE_HOURS_24XL_OPTIMIZED_MEMORY' + | 'COMPUTE_HOURS_24XL_HIGH_MEMORY' + | 'COMPUTE_HOURS_48XL' + | 'COMPUTE_HOURS_48XL_OPTIMIZED_CPU' + | 'COMPUTE_HOURS_48XL_OPTIMIZED_MEMORY' + | 'COMPUTE_HOURS_48XL_HIGH_MEMORY' | 'CUSTOM_DOMAIN' | 'PITR_7' | 'PITR_14' @@ -6558,6 +6567,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' | 'cd_default' | 'pitr_7' | 'pitr_14' @@ -6601,6 +6618,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' | 'cd_default' | 'pitr_7' | 'pitr_14' @@ -6679,6 +6704,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' inserted_at: string is_branch_enabled: boolean is_physical_backups_enabled: boolean @@ -6728,6 +6761,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' inserted_at: string | null is_branch_enabled: boolean is_physical_backups_enabled: boolean | null @@ -6815,7 +6856,6 @@ export interface components { } PutOAuthAppResponse: { client_id: string - client_secret_alias: string created_at: string icon?: string id: string @@ -6994,6 +7034,59 @@ export interface components { /** @enum {string} */ privilege_type: 'ALL' | 'SELECT' | 'INSERT' | 'UPDATE' | 'REFERENCES' }[] + RunLintByNameResponseDto: { + lints: { + cache_key: string + categories: ('PERFORMANCE' | 'SECURITY')[] + description: string + detail: string + /** @enum {string} */ + facing: 'EXTERNAL' + /** @enum {string} */ + level: 'ERROR' | 'WARN' | 'INFO' + metadata?: { + entity?: string + fkey_columns?: number[] + fkey_name?: string + name?: string + schema?: string + /** @enum {string} */ + type?: 'table' | 'view' | 'auth' | 'function' | 'extension' | 'compliance' + } + /** @enum {string} */ + name: + | 'unindexed_foreign_keys' + | 'auth_users_exposed' + | 'auth_rls_initplan' + | 'no_primary_key' + | 'unused_index' + | 'multiple_permissive_policies' + | 'policy_exists_rls_disabled' + | 'rls_enabled_no_policy' + | 'duplicate_index' + | 'security_definer_view' + | 'function_search_path_mutable' + | 'rls_disabled_in_public' + | 'extension_in_public' + | 'rls_references_user_metadata' + | 'materialized_view_in_api' + | 'foreign_table_in_api' + | 'unsupported_reg_types' + | 'auth_otp_long_expiry' + | 'auth_otp_short_length' + | 'ssl_not_enforced' + | 'network_restrictions_not_set' + | 'password_requirements_min_length' + | 'pitr_not_enabled' + | 'auth_leaked_password_protection' + | 'auth_insufficient_mfa_options' + | 'auth_password_policy_missing' + | 'leaked_service_key' + | 'no_backup_admin' + remediation: string + title: string + }[] + } RunQueryBody: { query: string } @@ -7047,57 +7140,8 @@ export interface components { postgrest?: string 'supabase-postgres': string } - SettingsResponse: { - project: { - cloud_provider: string - db_dns_name: string - db_host: string - db_name: string - db_port: number - db_user: string - id: number - inserted_at: string - jwt_secret: string - name: string - ref: string - region: string - services?: { - app: { - id: number - name: string - } - app_config: { - [key: string]: unknown - } - id: number - name: string - service_api_keys: { - api_key?: string - api_key_encrypted?: string - name: string - tags: string - }[] - }[] - ssl_enforced: boolean - status: string - } - services: { - app: { - id: number - name: string - } - app_config: { - [key: string]: unknown - } - id: number - name: string - service_api_keys: { - api_key?: string - api_key_encrypted?: string - name: string - tags: string - }[] - }[] + SetupIntentRequest: { + hcaptchaToken?: string } SetupIntentResponse: { client_secret: string @@ -7310,6 +7354,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' | 'cd_default' | 'pitr_7' | 'pitr_14' @@ -7362,6 +7414,10 @@ export interface components { description?: string name?: string } + UpdateCollectionBody: { + name: string + retention_days: number + } UpdateColumnBody: { check?: string comment?: string @@ -7488,6 +7544,7 @@ export interface components { EXTERNAL_TWITTER_CLIENT_ID?: string | null EXTERNAL_TWITTER_ENABLED?: boolean | null EXTERNAL_TWITTER_SECRET?: string | null + EXTERNAL_WEB3_SOLANA_ENABLED?: boolean | null EXTERNAL_WORKOS_CLIENT_ID?: string | null EXTERNAL_WORKOS_ENABLED?: boolean | null EXTERNAL_WORKOS_SECRET?: string | null @@ -7633,11 +7690,16 @@ export interface components { /** @enum {string} */ status: 'new' | 'seen' | 'archived' }[] - UpdateNotificationExceptionParamsDto: { + UpdateNotificationExceptionBodyDto: { /** Format: uuid */ - id: string - /** @description Project ref */ - ref: string + assigned_to?: string + is_disabled?: boolean + lint_category?: string + lint_metadata?: { + [key: string]: unknown + } + lint_name?: string + note?: string } UpdateNotificationsBodyV1: { ids: string[] @@ -8069,6 +8131,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8097,6 +8160,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8129,6 +8193,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8167,6 +8232,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8205,6 +8271,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8243,6 +8310,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8281,6 +8349,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8354,6 +8423,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8393,6 +8463,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -8426,6 +8497,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -8465,6 +8537,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -8620,6 +8693,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8708,6 +8782,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8738,6 +8813,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8768,6 +8844,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8798,6 +8875,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8826,6 +8904,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8858,6 +8937,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8918,8 +8998,8 @@ export interface operations { parameters: { query: { id: number - limit: number - offset: number + limit?: number + offset?: number } header: { 'x-connection-encrypted': string @@ -9077,10 +9157,7 @@ export interface operations { } IntegrationsController_getUserInstallations: { parameters: { - query: { - /** @description Filter results by integration name. Optional */ - integration_name: unknown - } + query?: never header?: never path?: never cookie?: never @@ -9613,12 +9690,12 @@ export interface operations { NotificationsController_getNotificationsV2: { parameters: { query?: { - limit?: string - offset?: string + limit?: number + offset?: number org_slug?: string - priority?: 'Critical' | 'Warning' | 'Info' + priority?: string project_ref?: string - status?: 'new' | 'seen' | 'archived' + status?: string } header?: never path?: never @@ -9751,6 +9828,7 @@ export interface operations { query?: never header?: never path: { + /** @description Oauth authorization id */ id: string } cookie?: never @@ -9828,6 +9906,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -9849,6 +9928,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -9881,6 +9961,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -9911,7 +9992,9 @@ export interface operations { OrgAuditLogsController_getAuditLogs: { parameters: { query: { + /** @description End timestamp */ iso_timestamp_end: string + /** @description Start timestamp */ iso_timestamp_start: string } header?: never @@ -9951,6 +10034,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -9989,6 +10073,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10025,12 +10110,13 @@ export interface operations { } OrgInvoicesController_getInvoices: { parameters: { - query: { - limit: string - offset: string + query?: { + limit?: number + offset?: number } header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10065,6 +10151,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10100,6 +10187,7 @@ export interface operations { header?: never path: { invoiceId: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10135,6 +10223,7 @@ export interface operations { header?: never path: { invoiceId: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10169,6 +10258,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10238,6 +10328,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10272,6 +10363,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10308,6 +10400,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10344,6 +10437,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10372,6 +10466,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10407,12 +10502,57 @@ export interface operations { parameters: { query: { endDate: string - metric: string + interval?: string + metric: + | 'EGRESS' + | 'DATABASE_SIZE' + | 'STORAGE_SIZE' + | 'MONTHLY_ACTIVE_USERS' + | 'MONTHLY_ACTIVE_SSO_USERS' + | 'FUNCTION_INVOCATIONS' + | 'STORAGE_IMAGES_TRANSFORMED' + | 'REALTIME_MESSAGE_COUNT' + | 'REALTIME_PEAK_CONNECTIONS' + | 'DISK_SIZE_GB_HOURS_GP3' + | 'DISK_SIZE_GB_HOURS_IO2' + | 'AUTH_MFA_PHONE' + | 'AUTH_MFA_WEB_AUTHN' + | 'LOG_DRAIN_EVENTS' + | 'MONTHLY_ACTIVE_THIRD_PARTY_USERS' + | 'DISK_THROUGHPUT_GP3' + | 'DISK_IOPS_GP3' + | 'DISK_IOPS_IO2' + | 'COMPUTE_HOURS_BRANCH' + | 'COMPUTE_HOURS_XS' + | 'COMPUTE_HOURS_SM' + | 'COMPUTE_HOURS_MD' + | 'COMPUTE_HOURS_L' + | 'COMPUTE_HOURS_XL' + | 'COMPUTE_HOURS_2XL' + | 'COMPUTE_HOURS_4XL' + | 'COMPUTE_HOURS_8XL' + | 'COMPUTE_HOURS_12XL' + | 'COMPUTE_HOURS_16XL' + | 'COMPUTE_HOURS_24XL' + | 'COMPUTE_HOURS_24XL_OPTIMIZED_CPU' + | 'COMPUTE_HOURS_24XL_OPTIMIZED_MEMORY' + | 'COMPUTE_HOURS_24XL_HIGH_MEMORY' + | 'COMPUTE_HOURS_48XL' + | 'COMPUTE_HOURS_48XL_OPTIMIZED_CPU' + | 'COMPUTE_HOURS_48XL_OPTIMIZED_MEMORY' + | 'COMPUTE_HOURS_48XL_HIGH_MEMORY' + | 'CUSTOM_DOMAIN' + | 'PITR_7' + | 'PITR_14' + | 'PITR_28' + | 'IPV4' + | 'LOG_DRAIN' projectRef?: string startDate: string } header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10445,6 +10585,7 @@ export interface operations { } header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10515,6 +10656,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10544,6 +10686,7 @@ export interface operations { header?: never path: { gotrue_id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10571,6 +10714,7 @@ export interface operations { header?: never path: { gotrue_id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10602,7 +10746,8 @@ export interface operations { header?: never path: { gotrue_id: string - role_id: string + role_id: number + /** @description Organization slug */ slug: string } cookie?: never @@ -10634,7 +10779,8 @@ export interface operations { header?: never path: { gotrue_id: string - role_id: string + role_id: number + /** @description Organization slug */ slug: string } cookie?: never @@ -10661,6 +10807,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10689,6 +10836,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10720,6 +10868,7 @@ export interface operations { header?: never path: { id: number + /** @description Organization slug */ slug: string } cookie?: never @@ -10746,6 +10895,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string token: string } @@ -10775,6 +10925,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string token: string } @@ -10797,6 +10948,92 @@ export interface operations { } } } + MfaController_getMFAState: { + parameters: { + query?: never + header?: never + path: { + /** @description Organization slug */ + slug: string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['MfaStatusResponseDto'] + } + } + /** @description MFA enfocement state on organization. */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['MfaStatusResponseDto'] + } + } + /** @description Failed to get organization MFA status */ + 500: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } + MfaController_updateMFAState: { + parameters: { + query?: never + header?: never + path: { + /** @description Organization slug */ + slug: string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['ChangeMFAEnforcementStateRequestDto'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['MfaStatusResponseDto'] + } + } + /** @description MFA enfocement state on organization updated successfully. */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['MfaStatusResponseDto'] + } + } + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Failed to update organization MFA enforcement */ + 500: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } ReachedFreeProjectLimitController_getMembersWhoReachedFreeProjectLimit: { parameters: { query?: never @@ -10833,6 +11070,7 @@ export interface operations { } header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10860,6 +11098,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10892,6 +11131,7 @@ export interface operations { header?: never path: { app_id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10920,6 +11160,7 @@ export interface operations { header?: never path: { app_id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10949,6 +11190,7 @@ export interface operations { path: { app_id: string secret_id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10975,6 +11217,7 @@ export interface operations { header?: never path: { id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -11007,6 +11250,7 @@ export interface operations { header?: never path: { id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -11035,6 +11279,7 @@ export interface operations { header?: never path: { id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -11062,7 +11307,9 @@ export interface operations { query?: never header?: never path: { + /** @description Oauth authorization id */ id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -11090,7 +11337,9 @@ export interface operations { query?: never header?: never path: { + /** @description Oauth authorization id */ id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -11118,6 +11367,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11152,6 +11402,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11188,6 +11439,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11229,11 +11481,7 @@ export interface operations { } cookie?: never } - requestBody: { - content: { - 'application/json': components['schemas']['HCaptchaBody'] - } - } + requestBody?: never responses: { 201: { headers: { @@ -11263,6 +11511,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11326,6 +11575,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11360,6 +11610,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11398,6 +11649,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11456,12 +11708,14 @@ export interface operations { OrgUsageController_getOrgUsage: { parameters: { query?: { + apikey?: string end?: string project_ref?: string start?: string } header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11513,6 +11767,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11549,6 +11804,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11589,6 +11845,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11632,6 +11889,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11668,6 +11926,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11704,14 +11963,15 @@ export interface operations { ColumnsController_deleteColumn: { parameters: { query: { + /** @description Boolean string, true or false */ cascade?: boolean - /** @description Column ID */ id: string } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11744,13 +12004,13 @@ export interface operations { ColumnsController_updateColumn: { parameters: { query: { - /** @description Column ID */ id: string } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11791,6 +12051,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11827,6 +12088,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11863,13 +12125,13 @@ export interface operations { ExtensionsController_deleteExtension: { parameters: { query: { - /** @description Extension ID */ id: string } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11911,6 +12173,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11950,6 +12213,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11986,6 +12250,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12022,13 +12287,13 @@ export interface operations { FunctionsController_deleteFunction: { parameters: { query: { - /** @description Function ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12061,13 +12326,13 @@ export interface operations { FunctionsController_updateFunction: { parameters: { query: { - /** @description Function ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12107,6 +12372,7 @@ export interface operations { excluded_schemas?: string id?: string include_columns?: string + include_system_schemas?: string included_schemas?: string limit?: string offset?: string @@ -12115,6 +12381,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12154,6 +12421,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12190,6 +12458,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12226,13 +12495,13 @@ export interface operations { PoliciesController_deletePolicy: { parameters: { query: { - /** @description Policy ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12265,13 +12534,13 @@ export interface operations { PoliciesController_updatePolicy: { parameters: { query: { - /** @description Policy ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12312,6 +12581,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12348,6 +12618,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12384,13 +12655,13 @@ export interface operations { PublicationsController_deletePublication: { parameters: { query: { - /** @description Publication ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12423,13 +12694,13 @@ export interface operations { PublicationsController_updatePublication: { parameters: { query: { - /** @description Publication ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12470,6 +12741,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12508,6 +12780,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12540,6 +12813,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12574,6 +12848,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12610,6 +12885,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12646,13 +12922,13 @@ export interface operations { SchemasController_deleteSchema: { parameters: { query: { - /** @description Schema ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12685,13 +12961,13 @@ export interface operations { SchemasController_updateSchema: { parameters: { query: { - /** @description Schema ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12740,6 +13016,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12776,6 +13053,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12812,14 +13090,15 @@ export interface operations { TablesController_deleteTable: { parameters: { query: { + /** @description Boolean string, true or false */ cascade?: boolean - /** @description Table ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12852,13 +13131,13 @@ export interface operations { TablesController_updateTable: { parameters: { query: { - /** @description Table ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12902,6 +13181,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12938,6 +13218,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12974,13 +13255,13 @@ export interface operations { TriggersController_deleteTrigger: { parameters: { query: { - /** @description Trigger ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13013,13 +13294,13 @@ export interface operations { TriggersController_updateTrigger: { parameters: { query: { - /** @description Trigger ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13063,6 +13344,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13107,6 +13389,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13310,7 +13593,6 @@ export interface operations { query?: never header?: never path: { - /** @description Access token ID */ id: number } cookie?: never @@ -13339,7 +13621,6 @@ export interface operations { query?: never header?: never path: { - /** @description Access token ID */ id: number } cookie?: never @@ -13366,7 +13647,9 @@ export interface operations { UserAuditLogsController_getAuditLogs: { parameters: { query: { + /** @description End timestamp */ iso_timestamp_end: string + /** @description Start timestamp */ iso_timestamp_start: string } header?: never @@ -13561,6 +13844,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13582,6 +13866,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13609,6 +13894,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13761,10 +14047,12 @@ export interface operations { query?: { iso_timestamp_end?: string iso_timestamp_start?: string + project?: string sql?: string } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13796,11 +14084,12 @@ export interface operations { } UsageApiController_getApiCounts: { parameters: { - query: { - interval: 'minutely' | 'hourly' | 'daily' | '15min' | '30min' | '1hr' | '3hr' | '1day' + query?: { + interval?: 'minutely' | 'hourly' | 'daily' | '15min' | '30min' | '1hr' | '3hr' | '1day' } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14111,6 +14400,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14145,13 +14435,14 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never } requestBody: { content: { - 'application/json': components['schemas']['LFSource'] + 'application/json': components['schemas']['CreateCollectionBody'] } } responses: { @@ -14183,6 +14474,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string token: string } @@ -14218,6 +14510,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string token: string } @@ -14253,6 +14546,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string token: string } @@ -14260,7 +14554,7 @@ export interface operations { } requestBody: { content: { - 'application/json': components['schemas']['LFSource'] + 'application/json': components['schemas']['UpdateCollectionBody'] } } responses: { @@ -14292,6 +14586,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string token: string variant: 'dot' | 'json_schema' @@ -14634,6 +14929,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14668,6 +14964,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14715,6 +15012,14 @@ export interface operations { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' | 'cd_default' | 'pitr_7' | 'pitr_14' @@ -14723,6 +15028,7 @@ export interface operations { | 'auth_mfa_phone_default' | 'auth_mfa_web_authn_default' | 'log_drain_default' + /** @description Project ref */ ref: string } cookie?: never @@ -14755,6 +15061,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14783,6 +15090,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14821,6 +15129,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14849,6 +15158,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14877,6 +15187,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14989,6 +15300,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15017,6 +15329,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15114,6 +15427,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15148,6 +15462,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15186,6 +15501,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15214,6 +15530,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15251,6 +15568,7 @@ export interface operations { parameters: { query?: { cursor?: string + /** @description Boolean string, true or false */ favorite?: boolean limit?: string name?: string @@ -15261,6 +15579,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15289,6 +15608,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15319,6 +15639,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15353,6 +15674,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15384,6 +15706,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15420,6 +15743,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15448,6 +15772,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15481,9 +15806,7 @@ export interface operations { ids: string[] } header?: never - path: { - ref: string - } + path?: never cookie?: never } requestBody?: never @@ -15516,6 +15839,7 @@ export interface operations { path: { /** @description Content folder id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -15546,6 +15870,7 @@ export interface operations { path: { /** @description Content folder id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -15577,6 +15902,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -15640,6 +15966,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15668,6 +15995,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15689,6 +16017,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15754,6 +16083,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15788,6 +16118,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15824,6 +16155,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15858,6 +16190,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15896,6 +16229,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15963,6 +16297,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16046,6 +16381,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16074,6 +16410,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16108,6 +16445,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16135,13 +16473,14 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never } requestBody: { content: { - 'application/json': components['schemas']['UpdateNotificationExceptionParamsDto'] + 'application/json': components['schemas']['UpdateNotificationExceptionBodyDto'] } } responses: { @@ -16251,6 +16590,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16318,6 +16658,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16356,6 +16697,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16390,6 +16732,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16447,6 +16790,63 @@ export interface operations { } } } + ProjectRunLintsController_runLintByName: { + parameters: { + query?: never + header?: never + path: { + name: + | 'unindexed_foreign_keys' + | 'auth_users_exposed' + | 'auth_rls_initplan' + | 'no_primary_key' + | 'unused_index' + | 'multiple_permissive_policies' + | 'policy_exists_rls_disabled' + | 'rls_enabled_no_policy' + | 'duplicate_index' + | 'security_definer_view' + | 'function_search_path_mutable' + | 'rls_disabled_in_public' + | 'extension_in_public' + | 'rls_references_user_metadata' + | 'materialized_view_in_api' + | 'foreign_table_in_api' + | 'unsupported_reg_types' + | 'auth_otp_long_expiry' + | 'auth_otp_short_length' + | 'ssl_not_enforced' + | 'network_restrictions_not_set' + | 'password_requirements_min_length' + | 'pitr_not_enabled' + | 'auth_leaked_password_protection' + | 'auth_insufficient_mfa_options' + | 'auth_password_policy_missing' + | 'leaked_service_key' + | 'no_backup_admin' + /** @description Project ref */ + ref: string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['RunLintByNameResponseDto'] + } + } + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } ProjectRunLintsController_runLeakedServiceKeyLint: { parameters: { query?: never @@ -16625,6 +17025,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16654,6 +17055,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16701,97 +17103,12 @@ export interface operations { } } } - ApiController_getProjectApi: { - parameters: { - query?: never - header?: never - path: { - /** @description Project ref */ - ref: string - } - cookie?: never - } - requestBody?: never - responses: { - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['ApiResponse'] - } - } - /** @description Failed to retrieve project's api info */ - 500: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } - JwtSecretUpdateStatusController_getJwtSecretUpdateStatus: { - parameters: { - query?: never - header?: never - path: { - /** @description Project ref */ - ref: string - } - cookie?: never - } - requestBody?: never - responses: { - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['JwtSecretUpdateStatus'] - } - } - /** @description Failed to retrieve JWT secret update status */ - 500: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } - PropsSettingsController_getProjectSettingsDeprecated: { - parameters: { - query?: never - header?: never - path: { - ref: string - } - cookie?: never - } - requestBody?: never - responses: { - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['SettingsResponse'] - } - } - /** @description Failed to retrieve project's settings */ - 500: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } ReplicationPipelinesController_getPipelines: { parameters: { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16826,6 +17143,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16865,7 +17183,9 @@ export interface operations { query?: never header?: never path: { + /** @description Pipeline id */ pipeline_id: number + /** @description Project ref */ ref: string } cookie?: never @@ -16901,7 +17221,9 @@ export interface operations { query?: never header?: never path: { + /** @description Pipeline id */ pipeline_id: number + /** @description Project ref */ ref: string } cookie?: never @@ -16938,7 +17260,9 @@ export interface operations { query?: never header?: never path: { + /** @description Pipeline id */ pipeline_id: number + /** @description Project ref */ ref: string } cookie?: never @@ -16971,7 +17295,9 @@ export interface operations { query?: never header?: never path: { + /** @description Pipeline id */ pipeline_id: number + /** @description Project ref */ ref: string } cookie?: never @@ -17004,7 +17330,9 @@ export interface operations { query?: never header?: never path: { + /** @description Pipeline id */ pipeline_id: number + /** @description Project ref */ ref: string } cookie?: never @@ -17039,7 +17367,9 @@ export interface operations { query?: never header?: never path: { + /** @description Pipeline id */ pipeline_id: number + /** @description Project ref */ ref: string } cookie?: never @@ -17072,6 +17402,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -17106,6 +17437,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -17145,7 +17477,9 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string + /** @description Sink id */ sink_id: number } cookie?: never @@ -17181,7 +17515,9 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string + /** @description Sink id */ sink_id: number } cookie?: never @@ -17218,7 +17554,9 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string + /** @description Sink id */ sink_id: number } cookie?: never @@ -17251,6 +17589,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -17286,6 +17625,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -17321,7 +17661,9 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string + /** @description Source id */ source_id: number } cookie?: never @@ -17357,7 +17699,9 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string + /** @description Source id */ source_id: number } cookie?: never @@ -17401,7 +17745,9 @@ export interface operations { query?: never header?: never path: { + /** @description Publication name */ publication_name: string + /** @description Project ref */ ref: string source_id: number } @@ -17436,7 +17782,9 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string + /** @description Source id */ source_id: number } cookie?: never @@ -17594,6 +17942,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -17628,6 +17977,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -17666,7 +18016,9 @@ export interface operations { query?: never header?: never path: { + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17701,7 +18053,9 @@ export interface operations { query?: never header?: never path: { + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17734,7 +18088,9 @@ export interface operations { query?: never header?: never path: { + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17771,7 +18127,9 @@ export interface operations { query?: never header?: never path: { + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17804,8 +18162,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17842,8 +18201,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17882,8 +18242,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17920,8 +18281,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17960,8 +18322,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17998,8 +18361,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -18038,8 +18402,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -18078,8 +18443,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -18118,6 +18484,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -18152,6 +18519,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -18190,7 +18558,9 @@ export interface operations { query?: never header?: never path: { + /** @description Storage credential id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -18246,7 +18616,7 @@ export interface operations { } requestBody: { content: { - 'application/json': components['schemas']['HCaptchaBody'] + 'application/json': components['schemas']['SetupIntentRequest'] } } responses: { @@ -18564,8 +18934,8 @@ export interface operations { } VercelProjectsController_getVercelProjects: { parameters: { - query: { - teamId: string + query?: { + teamId?: string } header: { vercel_authorization: string @@ -18594,8 +18964,8 @@ export interface operations { } VercelProjectsController_getVercelProject: { parameters: { - query: { - teamId: string + query?: { + teamId?: string } header: { vercel_authorization: string @@ -18628,7 +18998,7 @@ export interface operations { parameters: { query: { projectId: string - teamId: string + teamId?: string } header: { vercel_authorization: string @@ -18659,7 +19029,7 @@ export interface operations { parameters: { query: { projectId: string - teamId: string + teamId?: string } header: { vercel_authorization: string @@ -18721,9 +19091,11 @@ export interface operations { WorkflowRunController_listWorkflowRuns: { parameters: { query?: { + /** @description Branch ID */ branch_id?: string limit?: number offset?: number + /** @description Project ref */ project_ref?: string } header?: never @@ -18752,7 +19124,9 @@ export interface operations { WorkflowRunController_countWorkflowRuns: { parameters: { query?: { + /** @description Branch ID */ branch_id?: string + /** @description Project ref */ project_ref?: string } header?: never @@ -18783,6 +19157,7 @@ export interface operations { query?: never header?: never path: { + /** @description Workflow run ID */ workflow_run_id: string } cookie?: never diff --git a/packages/common/consent-state.ts b/packages/common/consent-state.ts new file mode 100644 index 00000000000..10b5b467992 --- /dev/null +++ b/packages/common/consent-state.ts @@ -0,0 +1,109 @@ +import type Usercentrics from '@usercentrics/cmp-browser-sdk' +import type { BaseCategory, UserDecision } from '@usercentrics/cmp-browser-sdk' +import { proxy, snapshot, useSnapshot } from 'valtio' +import { LOCAL_STORAGE_KEYS } from './constants' + +export const consentState = proxy({ + // Usercentrics state + UC: null as Usercentrics | null, + categories: null as BaseCategory[] | null, + + // Our state + showConsentToast: false, + hasConsented: false, + acceptAll: () => { + if (!consentState.UC) return + const previousConsentValue = consentState.hasConsented + + consentState.hasConsented = true + consentState.showConsentToast = false + + consentState.UC.acceptAllServices() + .then(() => { + consentState.categories = consentState.UC?.getCategoriesBaseInfo() ?? null + }) + .catch(() => { + consentState.hasConsented = previousConsentValue + consentState.showConsentToast = true + }) + }, + denyAll: () => { + if (!consentState.UC) return + const previousConsentValue = consentState.hasConsented + + consentState.hasConsented = false + consentState.showConsentToast = false + + consentState.UC.denyAllServices() + .then(() => { + consentState.categories = consentState.UC?.getCategoriesBaseInfo() ?? null + }) + .catch(() => { + consentState.showConsentToast = previousConsentValue + }) + }, + updateServices: (decisions: UserDecision[]) => { + if (!consentState.UC) return + + consentState.showConsentToast = false + + consentState.UC.updateServices(decisions) + .then(() => { + consentState.hasConsented = consentState.UC?.areAllConsentsAccepted() ?? false + consentState.categories = consentState.UC?.getCategoriesBaseInfo() ?? null + }) + .catch(() => { + consentState.showConsentToast = true + }) + }, +}) + +async function initUserCentrics() { + if (process.env.NODE_ENV === 'test') return + + const { default: Usercentrics } = await import('@usercentrics/cmp-browser-sdk') + + const UC = new Usercentrics(process.env.NEXT_PUBLIC_USERCENTRICS_RULESET_ID!, { + rulesetId: process.env.NEXT_PUBLIC_USERCENTRICS_RULESET_ID, + useRulesetId: true, + }) + + const initialUIValues = await UC.init() + + consentState.UC = UC + const hasConsented = UC.areAllConsentsAccepted() + + // 0 = first layer, aka show consent toast + consentState.showConsentToast = initialUIValues.initialLayer === 0 + consentState.hasConsented = hasConsented + consentState.categories = UC.getCategoriesBaseInfo() + + // If the user has previously consented (before usercentrics), accept all services + if (!hasConsented && localStorage?.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) === 'true') { + consentState.acceptAll() + localStorage.removeItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) + } +} + +// Usercentrics is not available on the server +if (typeof window !== 'undefined') { + initUserCentrics() +} + +// Public API for consent + +export function hasConsented() { + return snapshot(consentState).hasConsented +} + +export function useConsentState() { + const snap = useSnapshot(consentState) + + return { + hasAccepted: snap.hasConsented, + categories: snap.categories as BaseCategory[] | null, + acceptAll: snap.acceptAll, + denyAll: snap.denyAll, + updateServices: snap.updateServices, + } +} diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index 2aa48c9d1ad..f65187df517 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -1,4 +1,5 @@ export const LOCAL_STORAGE_KEYS = { + /** @deprecated – we're using usercentrics instead to handle telemetry consent */ TELEMETRY_CONSENT: 'supabase-consent-ph', TELEMETRY_DATA: 'supabase-telemetry-data', HIDE_PROMO_TOAST: 'supabase-hide-promo-toast-lw13-d1', diff --git a/packages/common/feature-flags.tsx b/packages/common/feature-flags.tsx index f79131f2fea..d05507c030a 100644 --- a/packages/common/feature-flags.tsx +++ b/packages/common/feature-flags.tsx @@ -1,9 +1,11 @@ +'use client' + import { FlagValues } from '@vercel/flags/react' import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' import { components } from 'api-types' import { useUser } from './auth' -import { LOCAL_STORAGE_KEYS } from './constants' +import { hasConsented } from './consent-state' import { get, post } from './fetchWrappers' import { ensurePlatformSuffix } from './helpers' @@ -17,10 +19,7 @@ export async function getFeatureFlags(API_URL: string) { } export async function trackFeatureFlag(API_URL: string, body: TrackFeatureFlagVariables) { - const consent = - (typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null) === 'true' + const consent = hasConsented() if (!consent) return undefined await post(`${ensurePlatformSuffix(API_URL)}/telemetry/feature-flags/track`, { body }) @@ -81,15 +80,21 @@ export const FeatureFlagProvider = ({ let flagStore: FeatureFlagContextType = { configcat: {}, posthog: {} } - // Load PH flags - const flags = await getFeatureFlags(API_URL) + // Run both async operations in parallel + const [flags, flagValues] = await Promise.all([ + getFeatureFlags(API_URL), + typeof getConfigCatFlags === 'function' + ? getConfigCatFlags(user?.email) + : Promise.resolve([]), + ]) + + // Process PostHog flags if (flags) { flagStore.posthog = flags } - // Load ConfigCat flags + // Process ConfigCat flags if (typeof getConfigCatFlags === 'function') { - const flagValues = await getConfigCatFlags(user?.email) let overridesCookieValue: Record = {} try { const cookies = getCookies() @@ -123,9 +128,9 @@ export const FeatureFlagProvider = ({ return ( - {/* + {/* [Joshen] Just support configcat flags in Vercel flags for now for simplicity - although I think it should be fairly simply to support PH too + although I think it should be fairly simply to support PH too */} {children} diff --git a/packages/common/helpers.ts b/packages/common/helpers.ts index 749eb556a4d..d48bd3a8b0f 100644 --- a/packages/common/helpers.ts +++ b/packages/common/helpers.ts @@ -1,3 +1,5 @@ +'use client' + import { useSyncExternalStore } from 'react' import type * as React from 'react' diff --git a/packages/common/hooks/index.ts b/packages/common/hooks/index.ts index 0e3a707f73b..5f6e518e4ac 100644 --- a/packages/common/hooks/index.ts +++ b/packages/common/hooks/index.ts @@ -1,3 +1,5 @@ +'use client' + export * from './useAnchorObserver' export * from './useBreakpoint' export * from './useConstant' diff --git a/packages/common/index.tsx b/packages/common/index.tsx index 25a77e6adb4..863f4ebbc46 100644 --- a/packages/common/index.tsx +++ b/packages/common/index.tsx @@ -1,6 +1,5 @@ -'use client' - export * from './auth' +export * from './consent-state' export * from './constants' export * from './database-types' export * from './gotrue' diff --git a/packages/common/package.json b/packages/common/package.json index 9acff427616..79f9e1be362 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -12,13 +12,15 @@ }, "dependencies": { "@types/dat.gui": "^0.7.12", + "@usercentrics/cmp-browser-sdk": "^4.42.0", "@vercel/flags": "^2.6.0", "api-types": "workspace:*", "config": "workspace:*", "dat.gui": "^0.7.9", "lodash": "^4.17.21", "next-themes": "^0.3.0", - "react-use": "^17.4.0" + "react-use": "^17.4.0", + "valtio": "catalog:" }, "devDependencies": { "@types/lodash": "4.17.5", diff --git a/packages/common/telemetry.tsx b/packages/common/telemetry.tsx index 18d46f585cf..d401d209d92 100644 --- a/packages/common/telemetry.tsx +++ b/packages/common/telemetry.tsx @@ -1,16 +1,22 @@ +'use client' + import { components } from 'api-types' import { useRouter } from 'next/compat/router' import { usePathname } from 'next/navigation' import { useCallback, useEffect, useRef } from 'react' +import { useLatest } from 'react-use' +import { useUser } from './auth' +import { hasConsented } from './consent-state' import { LOCAL_STORAGE_KEYS } from './constants' import { useFeatureFlags } from './feature-flags' import { post } from './fetchWrappers' import { ensurePlatformSuffix, isBrowser } from './helpers' import { useTelemetryCookie } from './hooks' import { TelemetryEvent } from './telemetry-constants' -import { useUser } from './auth' import { getSharedTelemetryData } from './telemetry-utils' +const { TELEMETRY_DATA } = LOCAL_STORAGE_KEYS + //--- // PAGE TELEMETRY //--- @@ -72,51 +78,67 @@ export const PageTelemetry = ({ const referrer = typeof document !== 'undefined' ? document?.referrer : '' useTelemetryCookie({ hasAcceptedConsent, title, referrer }) + const pathname = + pagesPathname ?? appPathname ?? (isBrowser ? window.location.pathname : undefined) + const pathnameRef = useLatest(pathname) + const featureFlagsRef = useLatest(featureFlags.posthog) + const sendPageTelemetry = useCallback(() => { if (!(enabled && hasAcceptedConsent)) return Promise.resolve() - return handlePageTelemetry( - API_URL, - pagesPathname ?? appPathname ?? undefined, - featureFlags.posthog - ).catch((e) => { + return handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current).catch((e) => { console.error('Problem sending telemetry page:', e) }) - }, [API_URL, pagesPathname, appPathname, hasAcceptedConsent, featureFlags.posthog]) + }, [API_URL, enabled, hasAcceptedConsent]) const sendPageLeaveTelemetry = useCallback(() => { if (!(enabled && hasAcceptedConsent)) return Promise.resolve() - return handlePageTelemetry( - API_URL, - pagesPathname ?? appPathname ?? undefined, - featureFlags.posthog - ).catch((e) => { + return handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current).catch((e) => { console.error('Problem sending telemetry page-leave:', e) }) - }, [pagesPathname, appPathname, hasAcceptedConsent, featureFlags.posthog]) + }, [API_URL, enabled, hasAcceptedConsent]) + // Handle initial page telemetry event const hasSentInitialPageTelemetryRef = useRef(false) - useEffect(() => { // Send page telemetry on first page load // Waiting for router ready before sending page_view // if not the path will be dynamic route instead of the browser url if ( (router?.isReady ?? true) && + hasAcceptedConsent && featureFlags.hasLoaded && !hasSentInitialPageTelemetryRef.current ) { - sendPageTelemetry() + const cookies = document.cookie.split(';') + const telemetryCookie = cookies.find((cookie) => cookie.trim().startsWith(TELEMETRY_DATA)) + if (telemetryCookie) { + try { + const encodedData = telemetryCookie.split('=')[1] + const telemetryData = JSON.parse(decodeURIComponent(encodedData)) + handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current, telemetryData) + // remove the telemetry cookie + document.cookie = `${TELEMETRY_DATA}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` + } catch (error) { + console.error('Invalid telemetry data:', error) + } + } else { + handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current) + } + hasSentInitialPageTelemetryRef.current = true } - }, [router?.isReady, featureFlags.hasLoaded]) + }, [router?.isReady, hasAcceptedConsent, featureFlags.hasLoaded]) useEffect(() => { // For pages router if (router === null) return function handleRouteChange() { + // Wait until we've sent the initial page telemetry event + if (!hasSentInitialPageTelemetryRef.current) return + sendPageTelemetry() } @@ -132,7 +154,8 @@ export const PageTelemetry = ({ // For app router if (router !== null) return - if (appPathname) { + // Wait until we've sent the initial page telemetry event + if (appPathname && !hasSentInitialPageTelemetryRef.current) { sendPageTelemetry() } }, [appPathname, router, sendPageTelemetry]) @@ -159,18 +182,21 @@ export const PageTelemetry = ({ type EventBody = components['schemas']['TelemetryEventBodyV2Dto'] export function sendTelemetryEvent(API_URL: string, event: TelemetryEvent, pathname?: string) { - const consent = - (typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null) === 'true' - + const consent = hasConsented() if (!consent) return const body: EventBody = { ...getSharedTelemetryData(pathname), action: event.action, custom_properties: 'properties' in event ? event.properties : {}, - groups: 'groups' in event ? event.groups : {}, + groups: 'groups' in event ? { ...event.groups } : {}, + } + + if (body.groups?.project === 'Unknown') { + delete body.groups.project + if (body.groups?.organization === 'Unknown') { + delete body.groups + } } return post(`${ensurePlatformSuffix(API_URL)}/telemetry/event`, body, { @@ -185,10 +211,7 @@ export function sendTelemetryEvent(API_URL: string, event: TelemetryEvent, pathn type IdentifyBody = components['schemas']['TelemetryIdentifyBodyV2'] export function sendTelemetryIdentify(API_URL: string, body: IdentifyBody) { - const consent = - (typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null) === 'true' + const consent = hasConsented() if (!consent) return Promise.resolve() diff --git a/packages/ui-patterns/ConsentToast/index.tsx b/packages/ui-patterns/ConsentToast/index.tsx index cdf639a05b1..aeb288b8f74 100644 --- a/packages/ui-patterns/ConsentToast/index.tsx +++ b/packages/ui-patterns/ConsentToast/index.tsx @@ -17,7 +17,7 @@ export const ConsentToast = ({ onAccept = noop, onOptOut = noop }: ConsentToastP

- We use first-party cookies to improve our services.{' '} + We use cookies to collect data and improve our services.{' '} ) } - -export { useConsent, useConsentValue } from '../shared/consent' diff --git a/packages/ui-patterns/FilterBar/FilterBar.tsx b/packages/ui-patterns/FilterBar/FilterBar.tsx new file mode 100644 index 00000000000..f1aa41910af --- /dev/null +++ b/packages/ui-patterns/FilterBar/FilterBar.tsx @@ -0,0 +1,1044 @@ +'use client' + +import React, { useRef, KeyboardEvent, useMemo, useState, useCallback, useEffect } from 'react' +import { Search, Sparkles } from 'lucide-react' +import { + Command_Shadcn_, + CommandEmpty_Shadcn_, + CommandGroup_Shadcn_, + CommandItem_Shadcn_, + CommandList_Shadcn_, + Dialog, + DialogContent, +} from 'ui' +import { FilterGroup as FilterGroupComponent } from './FilterGroup' +import { + FilterProperty, + FilterCondition, + FilterGroup, + CustomOptionProps, + FilterOption, + CustomOptionObject, + FilterOptionObject, + AsyncOptionsFunction, + SyncOptionsFunction, +} from './types' + +export type ActiveInput = + | { type: 'value'; path: number[] } + | { type: 'operator'; path: number[] } + | { type: 'group'; path: number[] } + | null + +type CommandItem = { + value: string + label: string + icon?: React.ReactNode + isCustom?: boolean + customOption?: (props: CustomOptionProps) => React.ReactElement +} + +function isCustomOptionObject(option: any): option is CustomOptionObject { + return typeof option === 'object' && 'component' in option +} + +function isFilterOptionObject(option: any): option is FilterOptionObject { + return typeof option === 'object' && 'value' in option && 'label' in option +} + +function isAsyncOptionsFunction( + options: FilterProperty['options'] +): options is AsyncOptionsFunction { + if (!options || Array.isArray(options) || isCustomOptionObject(options)) return false + return typeof options === 'function' && options.constructor.name === 'AsyncFunction' +} + +function isSyncOptionsFunction(options: FilterProperty['options']): options is SyncOptionsFunction { + if (!options || Array.isArray(options) || isCustomOptionObject(options)) return false + return typeof options === 'function' +} + +function isGroup(condition: FilterCondition | FilterGroup): condition is FilterGroup { + return 'logicalOperator' in condition +} + +export type FilterBarProps = { + filterProperties: FilterProperty[] + onFilterChange: (filters: FilterGroup) => void + freeformText: string + onFreeformTextChange: (freeformText: string) => void + filters: FilterGroup + aiApiUrl?: string +} + +export function FilterBar({ + filterProperties, + filters: activeFilters, + onFilterChange, + freeformText, + onFreeformTextChange, + aiApiUrl, +}: FilterBarProps) { + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const commandRef = useRef(null) + const [selectedCommandIndex, setSelectedCommandIndex] = useState(0) + const [isCommandMenuVisible, setIsCommandMenuVisible] = useState(false) + const hideTimeoutRef = useRef(null) + const [activeInput, setActiveInput] = useState(null) + const newPathRef = useRef([]) + const [loadingOptions, setLoadingOptions] = useState>({}) + const [propertyOptionsCache, setPropertyOptionsCache] = useState< + Record + >({}) + const loadTimeoutRef = useRef(null) + const [dialogContent, setDialogContent] = useState(null) + const [isDialogOpen, setIsDialogOpen] = useState(false) + const [pendingPath, setPendingPath] = useState(null) + + const findGroupByPath = (group: FilterGroup, path: number[]): FilterGroup | null => { + if (path.length === 0) return group + + const [current, ...rest] = path + const condition = group.conditions[current] + if (!condition) return null + + if (rest.length === 0) { + return isGroup(condition) ? condition : null + } + + if (isGroup(condition)) { + return findGroupByPath(condition, rest) + } + + return null + } + + const findConditionByPath = (group: FilterGroup, path: number[]): FilterCondition | null => { + if (path.length === 0) return null + + const [current, ...rest] = path + const condition = group.conditions[current] + if (!condition) return null + + if (rest.length === 0) { + return isGroup(condition) ? null : condition + } + + if (isGroup(condition)) { + return findConditionByPath(condition, rest) + } + + return null + } + + const handleInputChange = useCallback( + (path: number[], value: string) => { + console.log('handleInputChange called with path:', path, 'value:', value) + const updateNestedValue = ( + group: FilterGroup, + currentPath: number[], + newValue: string + ): FilterGroup => { + if (currentPath.length === 1) { + console.log('Updating value at path level 1:', currentPath[0], 'newValue:', newValue) + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === currentPath[0] ? { ...condition, value: newValue } : condition + ), + } + } + + const [current, ...rest] = currentPath + console.log('Updating nested value at path:', current, 'remaining path:', rest) + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current + ? isGroup(condition) + ? updateNestedValue(condition, rest, newValue) + : condition + : condition + ), + } + } + + const updatedFilters = updateNestedValue(activeFilters, path, value) + console.log('Updated filters:', updatedFilters) + onFilterChange(updatedFilters) + setSelectedCommandIndex(0) + setIsCommandMenuVisible(true) + }, + [activeFilters] + ) + + const handleOperatorChange = useCallback( + (path: number[], value: string) => { + const updateNestedOperator = ( + group: FilterGroup, + currentPath: number[], + newOperator: string + ): FilterGroup => { + if (currentPath.length === 1) { + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === currentPath[0] ? { ...condition, operator: newOperator } : condition + ), + } + } + + const [current, ...rest] = currentPath + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current + ? isGroup(condition) + ? updateNestedOperator(condition, rest, newOperator) + : condition + : condition + ), + } + } + + const updatedFilters = updateNestedOperator(activeFilters, path, value) + onFilterChange(updatedFilters) + setSelectedCommandIndex(0) + setIsCommandMenuVisible(true) + }, + [activeFilters] + ) + + const handleInputFocus = useCallback((path: number[]) => { + setActiveInput({ type: 'value', path }) + setIsCommandMenuVisible(true) + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current) + } + }, []) + + const handleOperatorFocus = useCallback((path: number[]) => { + setActiveInput({ type: 'operator', path }) + setIsCommandMenuVisible(true) + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current) + } + }, []) + + const handleGroupFreeformFocus = useCallback((path: number[]) => { + setActiveInput({ type: 'group', path }) + setIsCommandMenuVisible(true) + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current) + } + }, []) + + const handleInputBlur = useCallback(() => { + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current) + } + hideTimeoutRef.current = setTimeout(() => { + setIsCommandMenuVisible(false) + setActiveInput(null) + }, 150) + }, []) + + const handleAIFilter = async () => { + if (!activeInput || activeInput.type !== 'group' || !aiApiUrl) return + + setIsLoading(true) + setError(null) + + try { + const response = await fetch(aiApiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + prompt: freeformText, + filterProperties, + currentPath: activeInput.path, + }), + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'AI filtering failed') + } + + const data = await response.json() + console.log(data) + if (!data || !Array.isArray(data.conditions)) { + throw new Error('Invalid response from AI filter') + } + + // Process the nested structure + const processConditions = (conditions: any[]): any[] => { + return conditions.map((condition) => { + if (isGroup(condition)) { + // This is a group + return { + logicalOperator: condition.logicalOperator, + conditions: processConditions(condition.conditions), + } + } else { + // This is a condition + const matchedProperty = filterProperties.find( + (prop) => prop.name === condition.propertyName + ) + if (!matchedProperty) { + throw new Error(`Invalid property: ${condition.propertyName}`) + } + return { + propertyName: matchedProperty.name, + value: condition.value, + operator: condition.operator || '=', + } + } + }) + } + + const processedGroup = { + logicalOperator: data.logicalOperator || 'AND', + conditions: processConditions(data.conditions), + } + + // Update the active filters by replacing the group at the current path + const updateGroupAtPath = ( + group: FilterGroup, + path: number[], + newGroup: FilterGroup + ): FilterGroup => { + if (path.length === 0) { + return newGroup + } + + const [current, ...rest] = path + return { + ...group, + conditions: group.conditions.map((condition, index) => + index === current + ? updateGroupAtPath(condition as FilterGroup, rest, newGroup) + : condition + ), + } + } + + const updatedFilters = updateGroupAtPath(activeFilters, activeInput.path, processedGroup) + onFilterChange(updatedFilters) + + onFreeformTextChange('') + setIsCommandMenuVisible(false) + } catch (error: any) { + console.error('Error in AI filtering:', error) + setError(error.message || 'AI filtering failed. Please try again.') + onFreeformTextChange('') + } finally { + setIsLoading(false) + } + } + + const handleGroupFreeformChange = useCallback((path: number[], value: string) => { + onFreeformTextChange(value) + setSelectedCommandIndex(0) + setIsCommandMenuVisible(true) + }, []) + + const addFilterToGroup = useCallback( + (group: FilterGroup, path: number[], property: FilterProperty): FilterGroup => { + if (path.length === 0) { + return { + ...group, + conditions: [ + ...group.conditions, + { propertyName: property.name, value: null, operator: '=' }, + ], + } + } + + const [current, ...rest] = path + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current ? addFilterToGroup(condition as FilterGroup, rest, property) : condition + ), + } + }, + [] + ) + + const addGroupToGroup = useCallback((group: FilterGroup, path: number[]): FilterGroup => { + if (path.length === 0) { + return { + ...group, + conditions: [...group.conditions, { logicalOperator: 'AND', conditions: [] }], + } + } + + const [current, ...rest] = path + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current ? addGroupToGroup(condition as FilterGroup, rest) : condition + ), + } + }, []) + + const handleCustomOptionSelect = useCallback( + (property: FilterProperty, path: number[]) => { + if (property.options && isCustomOptionObject(property.options)) { + const element = property.options.component({ + onChange: (value: string) => { + handleInputChange(path, value) + setIsDialogOpen(false) + setDialogContent(null) + setPendingPath(null) + }, + onCancel: () => { + setIsDialogOpen(false) + setDialogContent(null) + setPendingPath(null) + }, + search: freeformText, + }) + setDialogContent(element) + setIsDialogOpen(true) + setPendingPath(path) + } + }, + [handleInputChange, freeformText] + ) + + const updateNestedFilter = ( + group: FilterGroup, + path: number[], + updateFn: (condition: FilterCondition) => FilterCondition + ): FilterGroup => { + if (path.length === 1) { + return { + ...group, + conditions: group.conditions.map((condition, index) => + index === path[0] ? updateFn(condition as FilterCondition) : condition + ), + } + } + + const [current, ...rest] = path + return { + ...group, + conditions: group.conditions.map((condition, index) => + index === current && isGroup(condition) + ? updateNestedFilter(condition, rest, updateFn) + : condition + ), + } + } + + const handleCommandSelect = (selectedValue: string) => { + if (selectedValue === 'ai-filter') { + handleAIFilter() + return + } + + if (selectedValue === 'group') { + if (activeInput && activeInput.type === 'group') { + const currentPath = activeInput.path + const group = findGroupByPath(activeFilters, currentPath) + if (!group) return + + const updatedFilters = addGroupToGroup(activeFilters, currentPath) + onFilterChange(updatedFilters) + newPathRef.current = [...currentPath, group.conditions.length] + setTimeout(() => { + setActiveInput({ type: 'group', path: newPathRef.current }) + setIsCommandMenuVisible(true) + }, 0) + onFreeformTextChange('') + } + return + } + + if (activeInput && activeInput.type === 'value') { + const path = activeInput.path + const selectedItem = commandItems.find((item) => item.value === selectedValue) as + | CommandItem + | undefined + + if (selectedItem?.isCustom && selectedItem.customOption) { + const element = selectedItem.customOption({ + onChange: (value: string) => { + console.log('onChange:', path, value) + handleInputChange(path, value) + setIsDialogOpen(false) + setDialogContent(null) + setPendingPath(null) + setTimeout(() => { + setActiveInput({ type: 'group', path: path.slice(0, -1) }) + setIsCommandMenuVisible(true) + }, 0) + }, + onCancel: () => { + setIsDialogOpen(false) + setDialogContent(null) + setPendingPath(null) + setTimeout(() => { + setActiveInput({ type: 'group', path: path.slice(0, -1) }) + setIsCommandMenuVisible(true) + }, 0) + }, + search: freeformText, + }) + setDialogContent(element) + setIsDialogOpen(true) + setPendingPath(path) + } else { + handleInputChange(path, selectedValue) + setTimeout(() => { + setActiveInput({ type: 'group', path: path.slice(0, -1) }) + setIsCommandMenuVisible(true) + }, 0) + } + } else if (activeInput && activeInput.type === 'operator') { + const path = activeInput.path + handleOperatorChange(path, selectedValue) + setActiveInput(null) + } else if (activeInput && activeInput.type === 'group') { + const selectedProperty = filterProperties.find((p) => p.name === selectedValue) + if (selectedProperty) { + const currentPath = activeInput.path + const group = findGroupByPath(activeFilters, currentPath) + if (!group) return + + // Check if the property itself is a custom option object + if ( + selectedProperty.options && + !Array.isArray(selectedProperty.options) && + isCustomOptionObject(selectedProperty.options) + ) { + // Add the filter first + const updatedFilters = addFilterToGroup(activeFilters, currentPath, selectedProperty) + onFilterChange(updatedFilters) + const newPath = [...currentPath, group.conditions.length] + + // Show the custom component immediately + const element = selectedProperty.options.component({ + onChange: (value: string) => { + console.log('Custom component onChange called with value:', value) + console.log('Current active input path:', newPath) + // Update the filter with the selected value using the nested update function + const filterWithValue = updateNestedFilter(updatedFilters, newPath, (condition) => ({ + ...condition, + propertyName: selectedProperty.name, + value: value, + operator: '=', + })) + onFilterChange(filterWithValue) + setIsDialogOpen(false) + setDialogContent(null) + setPendingPath(null) + setActiveInput({ type: 'group', path: currentPath }) + }, + onCancel: () => { + console.log('Custom component onCancel called') + removeFilterByPath(newPath) + setIsDialogOpen(false) + setDialogContent(null) + setPendingPath(null) + setActiveInput({ type: 'group', path: currentPath }) + }, + search: '', + }) + setDialogContent(element) + setIsDialogOpen(true) + setPendingPath(newPath) + setIsCommandMenuVisible(false) + } else { + // Handle normal array options or async options + const updatedFilters = addFilterToGroup(activeFilters, currentPath, selectedProperty) + onFilterChange(updatedFilters) + const newPath = [...currentPath, group.conditions.length] + + setTimeout(() => { + setActiveInput({ type: 'value', path: newPath }) + setIsCommandMenuVisible(true) + }, 0) + } + onFreeformTextChange('') + } else { + setError(`Invalid property: ${selectedValue}`) + } + } + } + + const loadPropertyOptions = useCallback( + async (property: FilterProperty, search: string = '') => { + // Skip if no options or if options is an array or not a function + if ( + !property.options || + Array.isArray(property.options) || + !isAsyncOptionsFunction(property.options) + ) + return + + // Check if we have cached options for this exact search + const cached = propertyOptionsCache[property.name] + if (cached && cached.searchValue === search) return + + // Clear any pending loads + if (loadTimeoutRef.current) { + clearTimeout(loadTimeoutRef.current) + } + + // Debounce the load + loadTimeoutRef.current = setTimeout(async () => { + if (loadingOptions[property.name]) return // Prevent duplicate loads + + try { + setLoadingOptions((prev) => ({ ...prev, [property.name]: true })) + // We can safely assert this is an AsyncOptionsFunction because we checked above + const asyncOptions = property.options as AsyncOptionsFunction + const rawOptions = await asyncOptions(search) + // Convert string options to FilterOptionObject format + const options = rawOptions.map((option: string | FilterOptionObject) => + typeof option === 'string' ? { label: option, value: option } : option + ) + setPropertyOptionsCache((prev) => ({ + ...prev, + [property.name]: { options, searchValue: search }, + })) + } catch (error) { + console.error(`Error loading options for ${property.name}:`, error) + setError(`Failed to load options for ${property.label}`) + } finally { + setLoadingOptions((prev) => ({ ...prev, [property.name]: false })) + } + }, 300) // Debounce time + }, + [loadingOptions, propertyOptionsCache] + ) + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (loadTimeoutRef.current) { + clearTimeout(loadTimeoutRef.current) + } + } + }, []) + + const commandItems = useMemo(() => { + if (activeInput?.type === 'operator') { + const condition = findConditionByPath(activeFilters, activeInput.path) + const property = filterProperties.find((p) => p.name === condition?.propertyName) + const operatorValue = condition?.operator?.toUpperCase() || '' + const availableOperators = property?.operators || ['='] + return availableOperators + .filter((op) => op.toUpperCase().includes(operatorValue)) + .map((op) => ({ value: op, label: op, icon: undefined })) + } + + const inputValue = + activeInput?.type === 'group' + ? freeformText + : activeInput?.type === 'value' + ? findConditionByPath(activeFilters, activeInput.path)?.value?.toString() || '' + : '' + + const items: CommandItem[] = [] + + if (activeInput?.type === 'group') { + items.push( + ...filterProperties + .filter((prop) => prop.label.toLowerCase().includes(inputValue.toLowerCase())) + .map((prop) => ({ + value: prop.name, + label: prop.label, + icon: undefined, + isCustom: false, + })) + ) + + items.push({ + value: 'group', + label: 'New Group', + icon: undefined, + }) + + if (inputValue.trim().length > 0 && aiApiUrl) { + items.push({ + value: 'ai-filter', + label: 'Filter by AI', + icon: , + }) + } + } else if (activeInput?.type === 'value') { + const activeCondition = findConditionByPath(activeFilters, activeInput.path) + const property = filterProperties.find((p) => p.name === activeCondition?.propertyName) + + if (property) { + // If the property itself is a custom option object, show just one item + if (!Array.isArray(property.options) && isCustomOptionObject(property.options)) { + items.push({ + value: 'custom', + label: property.options.label || 'Custom...', + icon: undefined, + isCustom: true, + customOption: property.options.component, + }) + } else if (loadingOptions[property.name]) { + items.push({ + value: 'loading', + label: 'Loading options...', + icon: undefined, + }) + } else if (Array.isArray(property.options)) { + items.push( + ...property.options + .filter((option) => { + if (typeof option === 'string') { + return option.toLowerCase().includes(inputValue.toLowerCase()) + } + if (isFilterOptionObject(option)) { + return option.label.toLowerCase().includes(inputValue.toLowerCase()) + } + if (isCustomOptionObject(option)) { + return option.label?.toLowerCase().includes(inputValue.toLowerCase()) ?? true + } + return true + }) + .map((option) => { + if (typeof option === 'string') { + return { + value: option, + label: option, + icon: undefined, + } as CommandItem + } + if (isFilterOptionObject(option)) { + return { + value: option.value, + label: option.label, + icon: undefined, + } as CommandItem + } + if (isCustomOptionObject(option)) { + return { + value: 'custom', + label: option.label || 'Custom...', + icon: undefined, + isCustom: true, + customOption: option.component, + } as CommandItem + } + return null + }) + .filter((item): item is CommandItem => item !== null) + ) + } else if (propertyOptionsCache[property.name]) { + items.push( + ...propertyOptionsCache[property.name].options.map((option) => { + if (typeof option === 'string') { + return { + value: option, + label: option, + icon: undefined, + } + } + return { + value: option.value, + label: option.label, + icon: undefined, + } + }) + ) + } + + // Load options if not cached or if search value has changed + if ( + typeof property.options === 'function' && + !loadingOptions[property.name] && + (!propertyOptionsCache[property.name] || + propertyOptionsCache[property.name].searchValue !== inputValue) + ) { + loadPropertyOptions(property, inputValue) + } + } + } + + return items + }, [ + activeInput, + freeformText, + activeFilters, + filterProperties, + propertyOptionsCache, + loadingOptions, + loadPropertyOptions, + aiApiUrl, + ]) + + const removeFilterByPath = useCallback( + (path: number[]) => { + const removeFromGroup = (group: FilterGroup, currentPath: number[]): FilterGroup => { + if (currentPath.length === 1) { + return { + ...group, + conditions: group.conditions.filter((_, i) => i !== currentPath[0]), + } + } + + const [current, ...rest] = currentPath + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current ? removeFromGroup(condition as FilterGroup, rest) : condition + ), + } + } + + const updatedFilters = removeFromGroup(activeFilters, path) + onFilterChange(updatedFilters) + }, + [activeFilters] + ) + + const removeGroupByPath = useCallback( + (path: number[]) => { + const removeFromGroup = (group: FilterGroup, currentPath: number[]): FilterGroup => { + if (currentPath.length === 1) { + return { + ...group, + conditions: group.conditions.filter((_, i) => i !== currentPath[0]), + } + } + + const [current, ...rest] = currentPath + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current ? removeFromGroup(condition as FilterGroup, rest) : condition + ), + } + } + + const updatedFilters = removeFromGroup(activeFilters, path) + onFilterChange(updatedFilters) + }, + [activeFilters] + ) + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault() + setSelectedCommandIndex((prevIndex) => + prevIndex < commandItems.length - 1 ? prevIndex + 1 : prevIndex + ) + } else if (e.key === 'ArrowUp') { + e.preventDefault() + setSelectedCommandIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : 0)) + } else if (e.key === 'Enter') { + e.preventDefault() + if (commandItems[selectedCommandIndex]) { + handleCommandSelect(commandItems[selectedCommandIndex].value) + } + } else if (e.key === 'Backspace') { + // Skip backspace handling for operator inputs + if (activeInput?.type === 'operator') return + + const inputElement = e.target as HTMLInputElement + const isEmpty = inputElement.value === '' + + if (activeInput?.type === 'group' && isEmpty) { + e.preventDefault() + const group = findGroupByPath(activeFilters, activeInput.path) + + if (group && group.conditions.length > 0) { + // Remove the last condition in the group + const lastConditionPath = [...activeInput.path, group.conditions.length - 1] + removeFilterByPath(lastConditionPath) + // Keep focus on the group's input + setActiveInput({ type: 'group', path: activeInput.path }) + } else if (group && group.conditions.length === 0) { + // Remove the empty group + removeGroupByPath(activeInput.path) + + // Focus the parent group's freeform input + if (activeInput.path.length > 0) { + setActiveInput({ + type: 'group', + path: activeInput.path.slice(0, -1), + }) + } else { + setActiveInput(null) + } + } + } else if (activeInput?.type === 'value' && isEmpty) { + const condition = findConditionByPath(activeFilters, activeInput.path) + if (condition && !condition.value) { + e.preventDefault() + removeFilterByPath(activeInput.path) + + // Focus the group's input + setActiveInput({ + type: 'group', + path: activeInput.path.slice(0, -1), + }) + } + } + } else if (e.key === ' ' && activeInput?.type === 'value') { + e.preventDefault() + setActiveInput({ type: 'group', path: [] }) + } else if (e.key === 'ArrowLeft') { + const inputElement = e.target as HTMLInputElement + if (inputElement.selectionStart === 0) { + e.preventDefault() + if (activeInput?.type === 'value') { + const lastIndex = activeInput.path[activeInput.path.length - 1] + if (lastIndex > 0) { + setActiveInput({ + type: 'value', + path: [...activeInput.path.slice(0, -1), lastIndex - 1], + }) + } else { + setActiveInput({ + type: 'operator', + path: activeInput.path, + }) + } + } else if (activeInput?.type === 'group') { + const group = findGroupByPath(activeFilters, activeInput.path) + if (group && group.conditions.length > 0) { + // If the group has conditions, select the last one + const lastConditionPath = [...activeInput.path, group.conditions.length - 1] + setActiveInput({ type: 'value', path: lastConditionPath }) + } else if (activeInput.path.length > 0) { + // If no conditions but has parent, select parent group's input + setActiveInput({ + type: 'group', + path: activeInput.path.slice(0, -1), + }) + } + } + } + } else if (e.key === 'ArrowRight') { + const inputElement = e.target as HTMLInputElement + if (inputElement.selectionStart === inputElement.value.length) { + e.preventDefault() + if (activeInput?.type === 'value') { + const group = findGroupByPath(activeFilters, activeInput.path.slice(0, -1)) + const lastIndex = activeInput.path[activeInput.path.length - 1] + if (group && lastIndex < group.conditions.length - 1) { + setActiveInput({ + type: 'value', + path: [...activeInput.path.slice(0, -1), lastIndex + 1], + }) + } else { + setActiveInput({ + type: 'group', + path: activeInput.path.slice(0, -1), + }) + } + } else if (activeInput?.type === 'operator') { + setActiveInput({ type: 'value', path: activeInput.path }) + } + } + } else if (e.key === 'Escape') { + setIsCommandMenuVisible(false) + setActiveInput(null) + } + } + + const handleLabelClick = useCallback((path: number[]) => { + setActiveInput({ type: 'value', path }) + }, []) + + const handleLogicalOperatorChange = useCallback( + (path: number[]) => { + const updateNestedLogicalOperator = ( + group: FilterGroup, + currentPath: number[] + ): FilterGroup => { + if (currentPath.length === 0) { + return { + ...group, + logicalOperator: group.logicalOperator === 'AND' ? 'OR' : 'AND', + } + } + + const [current, ...rest] = currentPath + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current + ? isGroup(condition) + ? updateNestedLogicalOperator(condition, rest) + : condition + : condition + ), + } + } + + const updatedFilters = updateNestedLogicalOperator(activeFilters, path) + onFilterChange(updatedFilters) + }, + [activeFilters] + ) + + return ( +

+ ) +} diff --git a/packages/ui-patterns/FilterBar/FilterCondition.tsx b/packages/ui-patterns/FilterBar/FilterCondition.tsx new file mode 100644 index 00000000000..f958534f860 --- /dev/null +++ b/packages/ui-patterns/FilterBar/FilterCondition.tsx @@ -0,0 +1,86 @@ +import React, { useRef, useEffect } from 'react' +import { Input_Shadcn_ } from 'ui' +import { FilterCondition as FilterConditionType, FilterProperty } from './types' + +type FilterConditionProps = { + condition: FilterConditionType + filterProperties: FilterProperty[] + id: string + isActive: boolean + isOperatorActive: boolean + isLoading?: boolean + onOperatorChange: (value: string) => void + onValueChange: (value: string) => void + onOperatorFocus: () => void + onValueFocus: () => void + onBlur: () => void + onLabelClick: () => void + onKeyDown: (e: React.KeyboardEvent) => void +} + +export function FilterCondition({ + condition, + filterProperties, + id, + isActive, + isOperatorActive, + isLoading, + onOperatorChange, + onValueChange, + onOperatorFocus, + onValueFocus, + onBlur, + onLabelClick, + onKeyDown, +}: FilterConditionProps) { + const operatorRef = useRef(null) + const valueRef = useRef(null) + const property = filterProperties.find((p) => p.name === condition.propertyName) + + useEffect(() => { + if (isActive && valueRef.current) { + valueRef.current.focus() + } else if (isOperatorActive && operatorRef.current) { + operatorRef.current.focus() + } + }, [isActive, isOperatorActive]) + + if (!property) return null + + return ( +
+ + {property.label} + + onOperatorChange(e.target.value)} + onFocus={onOperatorFocus} + onBlur={onBlur} + className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 font-mono h-6 mr-1 text-foreground-light" + style={{ + width: `${Math.max(condition.operator.length, 1)}ch`, + minWidth: '1ch', + }} + disabled={isLoading} + aria-label={`Operator for ${property.label}`} + /> + onValueChange(e.target.value)} + onFocus={onValueFocus} + onBlur={onBlur} + onKeyDown={onKeyDown} + className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 font-mono h-6" + style={{ + width: `${Math.max(condition.value?.toString().length || 0, 1)}ch`, + minWidth: '1ch', + }} + disabled={isLoading} + aria-label={`Value for ${property.label}`} + /> +
+ ) +} diff --git a/packages/ui-patterns/FilterBar/FilterGroup.tsx b/packages/ui-patterns/FilterBar/FilterGroup.tsx new file mode 100644 index 00000000000..8cd71755e36 --- /dev/null +++ b/packages/ui-patterns/FilterBar/FilterGroup.tsx @@ -0,0 +1,199 @@ +import React, { useState, useRef, useEffect } from 'react' +import { FilterProperty, FilterGroup as FilterGroupType } from './types' +import { ActiveInput } from './FilterBar' +import { FilterCondition } from './FilterCondition' +import { Input_Shadcn_ } from 'ui' + +type FilterGroupProps = { + group: FilterGroupType + path: number[] + isLoading?: boolean + filterProperties: FilterProperty[] + // Active state + activeInput: ActiveInput + onOperatorChange: (path: number[], value: string) => void + onValueChange: (path: number[], value: string) => void + onOperatorFocus: (path: number[]) => void + onValueFocus: (path: number[]) => void + onBlur: () => void + onLabelClick: (path: number[]) => void + onKeyDown: (e: React.KeyboardEvent) => void + // Group specific props + onGroupFreeformChange: (path: number[], value: string) => void + onGroupFreeformFocus: (path: number[]) => void + groupFreeformValue: string + isGroupFreeformActive: boolean + // Logical operator props + onLogicalOperatorChange?: (path: number[]) => void +} + +export function FilterGroup({ + group, + path, + isLoading, + activeInput, + filterProperties, + onOperatorChange, + onValueChange, + onOperatorFocus, + onValueFocus, + onBlur, + onLabelClick, + onKeyDown, + onGroupFreeformChange, + onGroupFreeformFocus, + groupFreeformValue, + isGroupFreeformActive, + onLogicalOperatorChange, +}: FilterGroupProps) { + const [localFreeformValue, setLocalFreeformValue] = useState('') + const freeformInputRef = useRef(null) + const [isHoveringOperator, setIsHoveringOperator] = useState(false) + const isActive = + isGroupFreeformActive && + activeInput?.type === 'group' && + path.length === activeInput.path.length && + path.every((v, i) => v === activeInput.path[i]) + + // Reset local value when group freeform value is cleared + useEffect(() => { + if (groupFreeformValue === '') { + setLocalFreeformValue('') + } + }, [groupFreeformValue]) + + useEffect(() => { + if (isActive && freeformInputRef.current) { + freeformInputRef.current.focus() + } + }, [isActive]) + + const handleFreeformChange = (e: React.ChangeEvent) => { + setLocalFreeformValue(e.target.value) + onGroupFreeformChange(path, e.target.value) + } + + const handleLogicalOperatorClick = () => { + onLogicalOperatorChange?.(path) + } + + const handleLogicalOperatorMouseEnter = () => { + setIsHoveringOperator(true) + } + + const handleLogicalOperatorMouseLeave = () => { + setIsHoveringOperator(false) + } + + const isConditionActive = (conditionPath: number[]) => { + if (!activeInput) return false + return ( + activeInput.type === 'value' && + conditionPath.length === activeInput.path.length && + conditionPath.every((v, i) => v === activeInput.path[i]) + ) + } + + const isOperatorActive = (conditionPath: number[]) => { + if (!activeInput) return false + return ( + activeInput.type === 'operator' && + conditionPath.length === activeInput.path.length && + conditionPath.every((v, i) => v === activeInput.path[i]) + ) + } + + return ( +
0 + ? "before:content-['('] before:text-foreground-muted after:content-[')'] after:text-foreground-muted" + : '' + }`} + > +
+ {group.conditions.map((condition, index) => { + const currentPath = [...path, index] + + return ( + + {index > 0 && ( + + {group.logicalOperator} + + )} + {/* Render condition or nested group */} + {'logicalOperator' in condition ? ( + + ) : ( + onOperatorChange(currentPath, value)} + filterProperties={filterProperties} + onValueChange={(value) => onValueChange(currentPath, value)} + onOperatorFocus={() => onOperatorFocus(currentPath)} + onValueFocus={() => onValueFocus(currentPath)} + onBlur={onBlur} + onLabelClick={() => onLabelClick(currentPath)} + onKeyDown={onKeyDown} + /> + )} + + ) + })} + {/* Add freeform input at the end */} + onGroupFreeformFocus(path)} + onBlur={onBlur} + onKeyDown={onKeyDown} + className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 font-mono h-6" + placeholder={ + path.length === 0 && group.conditions.length === 0 ? 'Search or filter...' : '+' + } + disabled={isLoading} + style={{ + width: `${Math.max( + (isActive ? groupFreeformValue : localFreeformValue).length || 1, + path.length === 0 && group.conditions.length === 0 ? 18 : 1 + )}ch`, + minWidth: path.length === 0 && group.conditions.length === 0 ? '18ch' : '1ch', + }} + /> +
+
+ ) +} diff --git a/packages/ui-patterns/FilterBar/index.ts b/packages/ui-patterns/FilterBar/index.ts new file mode 100644 index 00000000000..8f852e36afb --- /dev/null +++ b/packages/ui-patterns/FilterBar/index.ts @@ -0,0 +1,2 @@ +export * from './FilterBar' +export * from './types' diff --git a/packages/ui-patterns/FilterBar/types.ts b/packages/ui-patterns/FilterBar/types.ts new file mode 100644 index 00000000000..84be2d517ea --- /dev/null +++ b/packages/ui-patterns/FilterBar/types.ts @@ -0,0 +1,45 @@ +import React from 'react' + +export type CustomOptionProps = { + onChange: (value: string) => void + onCancel: () => void + search?: string +} + +export type FilterOptionObject = { + label: string + value: string +} + +export type CustomOptionObject = { + label?: string + component: (props: CustomOptionProps) => React.ReactElement +} + +export type FilterOption = string | FilterOptionObject | CustomOptionObject + +export type AsyncOptionsFunction = (search?: string) => Promise<(string | FilterOptionObject)[]> +export type SyncOptionsFunction = (search?: string) => (string | FilterOptionObject)[] + +export type FilterProperty = { + label: string + name: string + type: 'string' | 'number' | 'date' | 'boolean' + options?: FilterOption[] | AsyncOptionsFunction | SyncOptionsFunction | CustomOptionObject + operators?: string[] +} + +export type FilterCondition = { + propertyName: string + value: string | number | null + operator: string +} + +export type FilterGroup = { + logicalOperator: 'AND' | 'OR' + conditions: (FilterCondition | FilterGroup)[] +} + +export function isGroup(condition: FilterCondition | FilterGroup): condition is FilterGroup { + return 'logicalOperator' in condition +} diff --git a/packages/ui-patterns/PrivacySettings/index.tsx b/packages/ui-patterns/PrivacySettings/index.tsx index e1981ce5b66..ea1607b3fb7 100644 --- a/packages/ui-patterns/PrivacySettings/index.tsx +++ b/packages/ui-patterns/PrivacySettings/index.tsx @@ -1,40 +1,45 @@ 'use client' -import { handleResetTelemetry, LOCAL_STORAGE_KEYS } from 'common' +import { useConsentState } from 'common' import Link from 'next/link' -import { PropsWithChildren, useEffect, useState } from 'react' +import { PropsWithChildren, useState } from 'react' import { Modal, Toggle } from 'ui' -import { useConsentValue } from '../shared/consent' + +interface PrivacySettingsProps { + className?: string +} export const PrivacySettings = ({ children, ...props -}: PropsWithChildren<{ className?: string }>) => { +}: PropsWithChildren) => { const [isOpen, setIsOpen] = useState(false) - const { hasAccepted, handleConsent } = useConsentValue(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - const [telemetryValue, setTelemetryValue] = useState(hasAccepted) + const { categories, updateServices } = useConsentState() - // Every time the modal opens, sync state with localStorage - useEffect(() => { - setTelemetryValue(localStorage?.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) === 'true') - }, [isOpen]) + const [serviceConsentMap, setServiceConsentMap] = useState(() => new Map()) + + function handleServicesChange(services: { id: string; status: boolean }[]) { + let newServiceConsentMap = new Map(serviceConsentMap) + services.forEach((service) => { + newServiceConsentMap.set(service.id, service.status) + }) + setServiceConsentMap(newServiceConsentMap) + } const handleConfirmPreferences = () => { - handleConsent && handleConsent(telemetryValue ? 'true' : 'false') + const services = Array.from(serviceConsentMap.entries()).map(([id, status]) => ({ + serviceId: id, + status, + })) + updateServices(services) + setIsOpen(false) } const handleCancel = () => { - setTelemetryValue(hasAccepted) setIsOpen(false) } - const handleOptOutTelemetry = async () => { - // remove telemetry data from cookies - document.cookie = `${LOCAL_STORAGE_KEYS.TELEMETRY_DATA}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` - handleResetTelemetry(process.env.NEXT_PUBLIC_API_URL!) - } - return ( <>
- + /> */}
) } diff --git a/packages/ui-patterns/SqlToRest/index.tsx b/packages/ui-patterns/SqlToRest/index.tsx index 1e83262f392..75e91e42d22 100644 --- a/packages/ui-patterns/SqlToRest/index.tsx +++ b/packages/ui-patterns/SqlToRest/index.tsx @@ -25,7 +25,7 @@ function FallbackComponent({ error }: FallbackProps) { } // Lazy load client side to prevent hydration issues when browser produces an error -const SqlToRest = dynamic(() => import('./sql-to-rest')) +const SqlToRest = dynamic(() => import('./sql-to-rest'), { ssr: false }) export default function SqlToRestWithFallback(props: SqlToRestProps) { return ( diff --git a/packages/ui-patterns/admonition.tsx b/packages/ui-patterns/admonition.tsx index 5c546acbedd..6c046091cc0 100644 --- a/packages/ui-patterns/admonition.tsx +++ b/packages/ui-patterns/admonition.tsx @@ -103,7 +103,6 @@ export const Admonition = forwardRef< {...props} className={cn( 'mb-2', - '[&_a]:underline', admonitionSVG({ type: typeMapped }), admonitionBase({ type: typeMapped }), props.className diff --git a/packages/ui-patterns/consent.tsx b/packages/ui-patterns/consent.tsx new file mode 100644 index 00000000000..30079f08027 --- /dev/null +++ b/packages/ui-patterns/consent.tsx @@ -0,0 +1,59 @@ +'use client' + +import { consentState, isBrowser, LOCAL_STORAGE_KEYS } from 'common' +import { useCallback, useEffect, useRef } from 'react' +import { toast } from 'sonner' +import { cn } from 'ui' +import { useSnapshot } from 'valtio' +import { ConsentToast } from './ConsentToast' + +const { TELEMETRY_DATA } = LOCAL_STORAGE_KEYS + +export const useConsentToast = () => { + const consentToastId = useRef() + const snap = useSnapshot(consentState) + + const acceptAll = useCallback(() => { + if (!isBrowser) return + + snap.acceptAll() + + if (consentToastId.current) { + toast.dismiss(consentToastId.current) + } + }, [snap.acceptAll]) + + const denyAll = useCallback(() => { + if (!isBrowser) return + + snap.denyAll() + // remove the telemetry cookie + document.cookie = `${TELEMETRY_DATA}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` + + if (consentToastId.current) { + toast.dismiss(consentToastId.current) + } + }, [snap.denyAll]) + + useEffect(() => { + if (isBrowser && snap.showConsentToast) { + consentToastId.current = toast(, { + id: 'consent-toast', + position: 'bottom-right', + duration: Infinity, + closeButton: false, + dismissible: false, + className: cn( + '!w-screen !fixed !border-t !h-auto !left-0 !bottom-0 !top-auto !right-0 !rounded-none !max-w-none !bg-overlay !text', + 'sm:!w-full sm:!max-w-[356px] sm:!left-auto sm:!right-8 sm:!bottom-8 sm:!rounded-lg sm:border' + ), + }) + } else if (consentToastId.current) { + toast.dismiss(consentToastId.current) + } + }, [snap.showConsentToast]) + + return { + hasAcceptedConsent: snap.hasConsented, + } +} diff --git a/packages/ui-patterns/index.tsx b/packages/ui-patterns/index.tsx index 8bc8556609b..5ef7583b73c 100644 --- a/packages/ui-patterns/index.tsx +++ b/packages/ui-patterns/index.tsx @@ -3,7 +3,7 @@ * import directly from here. */ export * from './CommandMenu' -export * from './ConsentToast' +export * from './consent' export * from './CountdownWidget' export * from './ExpandableVideo' export * from './GlassPanel' @@ -22,5 +22,6 @@ export * from './PromoToast' export * from './admonition' export * from './ComputeBadge' export * from './TimestampInfo' +export * from './FilterBar' export * from './Toc' export * from './LogsBarChart' diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index 9c33161b2d2..87724d91eeb 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -48,7 +48,7 @@ "tsconfig": "workspace:*", "ui": "workspace:*", "unist-util-visit": "^5.0.0", - "valtio": "*", + "valtio": "catalog:", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/ui-patterns/shared/consent.tsx b/packages/ui-patterns/shared/consent.tsx deleted file mode 100644 index a3e57172326..00000000000 --- a/packages/ui-patterns/shared/consent.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { LOCAL_STORAGE_KEYS, handlePageTelemetry, isBrowser } from 'common' -import { useCallback, useEffect, useRef, useState } from 'react' -import { toast } from 'sonner' -import { proxy, useSnapshot } from 'valtio' -import { useRouter } from 'next/compat/router' -import { usePathname } from 'next/navigation' -import { useFeatureFlags } from 'common' -import { cn } from 'ui' -import { ConsentToast } from '../ConsentToast' - -const consentState = proxy({ - consentValue: (isBrowser ? localStorage?.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) : null) as - | string - | null, - setConsentValue: (value: string | null) => { - consentState.consentValue = value - }, -}) - -export const useConsent = () => { - const { TELEMETRY_CONSENT, TELEMETRY_DATA } = LOCAL_STORAGE_KEYS - const consentToastId = useRef() - const router = useRouter() - const appRouterPathname = usePathname() - const featureFlags = useFeatureFlags() - - const { consentValue } = useSnapshot(consentState) - const pathname = - router?.pathname ?? appRouterPathname ?? (isBrowser ? window.location.pathname : '') - - const handleConsent = (value: 'true' | 'false') => { - if (!isBrowser) return - - if (value === 'true') { - const cookies = document.cookie.split(';') - const telemetryCookie = cookies.find((cookie) => cookie.trim().startsWith(TELEMETRY_DATA)) - if (telemetryCookie) { - try { - const encodedData = telemetryCookie.split('=')[1] - const telemetryData = JSON.parse(decodeURIComponent(encodedData)) - handlePageTelemetry( - process.env.NEXT_PUBLIC_API_URL!, - pathname, - featureFlags.posthog, - telemetryData - ) - // remove the telemetry cookie - document.cookie = `${TELEMETRY_DATA}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` - } catch (error) { - console.error('Invalid telemetry data:', error) - } - } else { - handlePageTelemetry(process.env.NEXT_PUBLIC_API_URL!, pathname, featureFlags.posthog) - } - } else { - // remove the telemetry cookie - document.cookie = `${TELEMETRY_DATA}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` - } - - consentState.setConsentValue(value) - localStorage.setItem(TELEMETRY_CONSENT, value) - - if (consentToastId.current) { - toast.dismiss(consentToastId.current) - } - } - - const triggerConsentToast = useCallback(() => { - if (isBrowser && consentValue === null) { - consentToastId.current = toast( - handleConsent('true')} - onOptOut={() => handleConsent('false')} - />, - { - id: 'consent-toast', - position: 'bottom-right', - duration: Infinity, - closeButton: false, - dismissible: false, - className: cn( - '!w-screen !fixed !border-t !h-auto !left-0 !bottom-0 !top-auto !right-0 !rounded-none !max-w-none !bg-overlay !text', - 'sm:!w-full sm:!max-w-[356px] sm:!left-auto sm:!right-8 sm:!bottom-8 sm:!rounded-lg sm:border' - ), - } - ) - } - }, []) - - useEffect(() => { - const handleSetLocalStorage = () => { - if (localStorage?.getItem(TELEMETRY_CONSENT)) toast.dismiss(consentToastId.current) - } - - if (isBrowser) { - window.addEventListener('storage', handleSetLocalStorage) - return window.removeEventListener('storage', () => null) - } - }, []) - - useEffect(() => { - setTimeout(() => { - consentValue === null && triggerConsentToast() - }, 300) - }, [consentValue]) - - return { - consentValue, - hasAcceptedConsent: consentValue === 'true', - triggerConsentToast, - } -} - -export const useConsentValue = (KEY_NAME: string) => { - const initialValue = isBrowser ? localStorage?.getItem(KEY_NAME) : null - const [consentValue, setConsentValue] = useState(initialValue) - - const handleConsent = (value: 'true' | 'false') => { - if (!isBrowser) return - setConsentValue(value) - localStorage.setItem(KEY_NAME, value) - window.dispatchEvent(new Event('storage')) - if (value === 'true') { - handlePageTelemetry(process.env.NEXT_PUBLIC_API_URL!, location.pathname) - } - } - - return { - consentValue, - setConsentValue, - hasAccepted: consentValue === 'true', - handleConsent, - } -} diff --git a/packages/ui/src/components/TreeView/TreeView.tsx b/packages/ui/src/components/TreeView/TreeView.tsx index 7ecc3435566..d8003cf382d 100644 --- a/packages/ui/src/components/TreeView/TreeView.tsx +++ b/packages/ui/src/components/TreeView/TreeView.tsx @@ -2,7 +2,15 @@ import { cva, VariantProps } from 'class-variance-authority' import { ChevronRight, FolderClosed, FolderOpen, Loader2 } from 'lucide-react' -import { ComponentPropsWithoutRef, forwardRef, ReactNode, useEffect, useRef, useState } from 'react' +import { + ComponentPropsWithoutRef, + FocusEvent, + forwardRef, + ReactNode, + useEffect, + useRef, + useState, +} from 'react' import TreeViewPrimitive, { flattenTree } from 'react-accessible-treeview' import { cn } from '../../lib/utils' import { Input } from '../shadcn/ui/input' @@ -90,41 +98,44 @@ const TreeViewItem = forwardRef< ) => { const [localValueState, setLocalValueState] = useState(name) const inputRef = useRef(null) - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (inputRef.current && !inputRef.current.contains(event.target as Node)) { - onEditSubmit?.(localValueState) - } - } - if (isEditing) document.addEventListener('mousedown', handleClickOutside) - return () => { - if (isEditing) document.removeEventListener('mousedown', handleClickOutside) - } - }, [isEditing]) + const timeRef = useRef(0) useEffect(() => { if (isEditing) { - inputRef.current?.focus() + // [Ivan] This component is supposed to focus on its input when it's rendered. The focus doesn't work every time because + // the initial render is triggered by a dropdown menu which at the end of the closing animation, steals focus from the input + // and triggers the blur event (which closes the input). The issue is reported at https://github.com/radix-ui/primitives/issues/3106. - // When editing starts, select text up to the last dot - if (inputRef.current) { + // [Joshen] This is to prevent accidental onBlur callbacks by checking that the onBlur event is being triggered + // within 400ms of the input field being in an edit state. 400ms is just an arbitary value which I think + // represents an "accidental" on blur + timeRef.current = Number(new Date()) + + // The focus will be attempted after a slight delay to ensure that the dropdown closing animations are complete. + setTimeout(() => { const input = inputRef.current - - // Need a slight delay to ensure focus is established - setTimeout(() => { - const fileName = input.value - const lastDotIndex = fileName.lastIndexOf('.') - const startPos = 0 - const endPos = lastDotIndex > 0 ? lastDotIndex : fileName.length - - try { - input.setSelectionRange(startPos, endPos) - } catch (e) { - console.error('Could not set selection range', e) + if (input) { + // If the input is not the active element, focus it + if (document.activeElement !== input) { + input.focus() } - }, 50) - } + + // Need a slight delay to ensure focus is established. When editing starts, select text up to the last dot + // When editing starts, select text up to the last dot + setTimeout(() => { + const fileName = input.value + const lastDotIndex = fileName.lastIndexOf('.') + const startPos = 0 + const endPos = lastDotIndex > 0 ? lastDotIndex : fileName.length + + try { + input.setSelectionRange(startPos, endPos) + } catch (e) { + console.error('Could not set selection range', e) + } + }, 50) + } + }, 200) } else { setLocalValueState(name) } @@ -136,8 +147,16 @@ const TreeViewItem = forwardRef< } }, [isLoading]) - const handleBlur = () => { - onEditSubmit?.(localValueState) + const handleBlur = (e: FocusEvent) => { + const timestamp = Number(new Date()) + const timeDiff = timestamp - timeRef.current + + if (timeDiff < 400) { + e.preventDefault() + inputRef.current?.focus() + } else { + onEditSubmit?.(localValueState) + } } const handleSubmit = (e: React.FormEvent) => { @@ -228,8 +247,9 @@ const TreeViewItem = forwardRef< {name} -
+ { setLocalValueState(e.target.value) @@ -239,7 +259,7 @@ const TreeViewItem = forwardRef< // stop keyboard down bubbling up to TreeView.root // on enter key, send onEditSubmit callback if (e.key === 'Enter') { - onEditSubmit?.(localValueState) + inputRef.current?.blur() } else if (e.key === 'Escape') { setLocalValueState(name) onEditSubmit?.(name) @@ -247,7 +267,7 @@ const TreeViewItem = forwardRef< e.stopPropagation() } }} - className="block w-full text-sm px-2 py-1 h-7 w" + className="block w-full text-sm px-2 py-1 h-7" value={localValueState} />
diff --git a/packages/ui/src/components/shadcn/ui/alert-dialog.tsx b/packages/ui/src/components/shadcn/ui/alert-dialog.tsx index d75c6e17d6a..80997e3820c 100644 --- a/packages/ui/src/components/shadcn/ui/alert-dialog.tsx +++ b/packages/ui/src/components/shadcn/ui/alert-dialog.tsx @@ -5,6 +5,8 @@ import * as React from 'react' import { cn } from '../../../lib/utils/cn' import { buttonVariants } from './../../Button' +import { VariantProps } from 'class-variance-authority' +import { cva } from 'class-variance-authority' const AlertDialog = AlertDialogPrimitive.Root @@ -21,33 +23,68 @@ AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName const AlertDialogOverlay = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { centered?: boolean } +>(({ className, centered = true, ...props }, ref) => ( )) AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName +const AlertDialogContentVariants = cva( + cn( + 'relative z-50 w-full max-w-screen border shadow-md dark:shadow-sm', + 'data-[state=open]:animate-in data-[state=closed]:animate-out', + 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95', + 'data-[state=closed]:slide-out-to-left-[0%] data-[state=closed]:slide-out-to-top-[0%]', + 'data-[state=open]:slide-in-from-left-[0%] data-[state=open]:slide-in-from-top-[0%]', + 'sm:rounded-lg md:w-full', + 'bg-dash-sidebar' + ), + { + variants: { + size: { + tiny: `sm:align-middle sm:w-full sm:max-w-xs`, + small: `sm:align-middle sm:w-full sm:max-w-sm`, + medium: `sm:align-middle sm:w-full sm:max-w-lg`, + large: `sm:align-middle sm:w-full md:max-w-xl`, + xlarge: `sm:align-middle sm:w-full md:max-w-3xl`, + xxlarge: `sm:align-middle sm:w-full md:max-w-6xl`, + xxxlarge: `sm:align-middle sm:w-full md:max-w-7xl`, + }, + }, + defaultVariants: { + size: 'medium', + }, + } +) + const AlertDialogContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( + React.ComponentPropsWithoutRef & + VariantProps & { + hideClose?: boolean + dialogOverlayProps?: React.ComponentPropsWithoutRef + centered?: boolean + } +>(({ className, children, size, dialogOverlayProps, centered = true, ...props }, ref) => ( - - + + + {children} + + )) AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName @@ -59,7 +96,10 @@ AlertDialogHeader.displayName = 'AlertDialogHeader' const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
) @@ -71,7 +111,7 @@ const AlertDialogTitle = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -83,7 +123,7 @@ const AlertDialogDescription = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -95,7 +135,7 @@ const AlertDialogAction = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -107,7 +147,7 @@ const AlertDialogCancel = React.forwardRef< >(({ className, ...props }, ref) => ( )) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2afa5c9f27..e2cb65802f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ catalogs: react-dom: specifier: ^18.3.0 version: 18.3.1 + valtio: + specifier: ^1.12.0 + version: 1.12.0 overrides: '@supabase/supabase-js>@supabase/auth-js': 2.69.0-rc.3 @@ -282,6 +285,12 @@ importers: github-slugger: specifier: ^2.0.0 version: 2.0.0 + graphql: + specifier: ^16.10.0 + version: 16.10.0 + graphql-validation-complexity: + specifier: ^0.4.2 + version: 0.4.2(graphql@16.10.0) gray-matter: specifier: ^4.0.3 version: 4.0.3 @@ -359,7 +368,7 @@ importers: version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nuqs: specifier: ^1.19.1 - version: 1.19.1(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)) + version: 1.19.1(next@14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)) openai: specifier: ^4.20.1 version: 4.71.1(encoding@0.1.13)(zod@3.23.8) @@ -433,7 +442,7 @@ importers: specifier: ^9.0.1 version: 9.0.1 valtio: - specifier: ^1.12.0 + specifier: 'catalog:' version: 1.12.0(@types/react@18.3.3)(react@18.3.1) yaml: specifier: ^2.4.5 @@ -445,6 +454,18 @@ importers: '@aws-sdk/client-secrets-manager': specifier: ^3.410.0 version: 3.468.0 + '@graphiql/toolkit': + specifier: ^0.9.1 + version: 0.9.1(@types/node@20.12.11)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0) + '@graphql-codegen/cli': + specifier: 5.0.5 + version: 5.0.5(@parcel/watcher@2.5.1)(@types/node@20.12.11)(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0)(supports-color@8.1.1)(typescript@5.5.2) + '@graphql-codegen/typescript': + specifier: 4.1.6 + version: 4.1.6(encoding@0.1.13)(graphql@16.10.0) + '@graphql-codegen/typescript-resolvers': + specifier: 4.5.0 + version: 4.5.0(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0) '@supabase/supa-mdx-lint': specifier: 0.2.6-alpha version: 0.2.6-alpha @@ -454,6 +475,9 @@ importers: '@types/estree': specifier: 1.0.5 version: 1.0.5 + '@types/graphql-validation-complexity': + specifier: ^0.4.4 + version: 0.4.4 '@types/hast': specifier: ^2.3.4 version: 2.3.6 @@ -499,6 +523,9 @@ importers: globby: specifier: ^13.2.2 version: 13.2.2 + graphiql: + specifier: ^3.8.3 + version: 3.8.3(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) openapi-types: specifier: ^12.1.3 version: 12.1.3 @@ -564,10 +591,10 @@ importers: version: 0.2.15 '@graphiql/react': specifier: ^0.19.4 - version: 0.19.4(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.8.1))(graphql@16.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 0.19.4(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@graphiql/toolkit': specifier: ^0.9.1 - version: 0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.8.1))(graphql@16.8.1) + version: 0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0) '@gregnr/postgres-meta': specifier: ^0.82.0-dev.2 version: 0.82.0-dev.2(encoding@0.1.13)(supports-color@8.1.1) @@ -732,7 +759,7 @@ importers: version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nuqs: specifier: ^2.4.1 - version: 2.4.1(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 2.4.1(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) openai: specifier: ^4.20.1 version: 4.71.1(encoding@0.1.13)(zod@3.23.8) @@ -796,6 +823,9 @@ importers: react-markdown: specifier: ^8.0.3 version: 8.0.7(@types/react@18.3.3)(react@18.3.1)(supports-color@8.1.1) + react-resizable: + specifier: 3.0.5 + version: 3.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-simple-maps: specifier: 4.0.0-beta.6 version: 4.0.0-beta.6(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -848,7 +878,7 @@ importers: specifier: ^9.0.1 version: 9.0.1 valtio: - specifier: ^1.12.0 + specifier: 'catalog:' version: 1.12.0(@types/react@18.3.3)(react@18.3.1) yup: specifier: ^1.4.0 @@ -976,7 +1006,7 @@ importers: version: 2.0.7(eslint@8.57.0(supports-color@8.1.1)) graphql-ws: specifier: 5.14.1 - version: 5.14.1(graphql@16.8.1) + version: 5.14.1(graphql@16.10.0) import-in-the-middle: specifier: ^1.13.1 version: 1.13.1 @@ -1039,7 +1069,7 @@ importers: version: 1.1.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-router/fs-routes': specifier: ^7.4.0 - version: 7.4.0(@react-router/dev@7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5))(typescript@5.5.2) + version: 7.4.0(@react-router/dev@7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5))(typescript@5.5.2) '@supabase/postgrest-js': specifier: '*' version: 1.19.2 @@ -1130,7 +1160,7 @@ importers: devDependencies: '@react-router/dev': specifier: ^7.1.5 - version: 7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5) + version: 7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5) '@shikijs/compat': specifier: ^1.1.7 version: 1.6.0 @@ -1174,8 +1204,8 @@ importers: specifier: ^14.3.8 version: 14.3.8(react@18.3.1) react-router: - specifier: ^7.4.0 - version: 7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.5.2 + version: 7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rimraf: specifier: ^4.1.3 version: 4.4.1 @@ -1496,6 +1526,9 @@ importers: '@types/common-tags': specifier: ^1.8.4 version: 1.8.4 + '@types/mdast': + specifier: ^4.0.0 + version: 4.0.1 '@types/node': specifier: 'catalog:' version: 20.12.11 @@ -1562,9 +1595,12 @@ importers: '@types/dat.gui': specifier: ^0.7.12 version: 0.7.12 + '@usercentrics/cmp-browser-sdk': + specifier: ^4.42.0 + version: 4.42.0 '@vercel/flags': specifier: ^2.6.0 - version: 2.6.0(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.6.0(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) api-types: specifier: workspace:* version: link:../api-types @@ -1579,7 +1615,7 @@ importers: version: 4.17.21 next: specifier: 'catalog:' - version: 14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0) + version: 15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1592,6 +1628,9 @@ importers: react-use: specifier: ^17.4.0 version: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + valtio: + specifier: 'catalog:' + version: 1.12.0(@types/react@18.3.3)(react@18.3.1) devDependencies: '@types/lodash': specifier: 4.17.5 @@ -2014,7 +2053,7 @@ importers: version: 0.52.2 next: specifier: '*' - version: 14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0) + version: 15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0) next-themes: specifier: '*' version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2082,7 +2121,7 @@ importers: specifier: ^5.0.0 version: 5.0.0 valtio: - specifier: '*' + specifier: 'catalog:' version: 1.12.0(@types/react@18.3.3)(react@18.3.1) zod: specifier: ^3.22.4 @@ -2123,7 +2162,7 @@ importers: version: link:../api-types next-router-mock: specifier: ^0.9.13 - version: 0.9.13(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react@18.3.1) + version: 0.9.13(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react@18.3.1) typescript: specifier: ~5.5.0 version: 5.5.2 @@ -2240,6 +2279,12 @@ packages: resolution: {integrity: sha512-C90iyzm/jLV7Lomv2UzwWUzRv9WZr1oRsFRKsX5HjQL4EXrbi9H/RtBkjCP+NF+ABZXUKpAa4F1dkoTaea4zHg==} hasBin: true + '@ardatan/relay-compiler@12.0.3': + resolution: {integrity: sha512-mBDFOGvAoVlWaWqs3hm1AciGHSQE1rqFc/liZTyYz/Oek9yZdT5H26pH2zAFuEiTiBVPPyMuqf5VjOFPI2DGsQ==} + hasBin: true + peerDependencies: + graphql: '*' + '@aws-crypto/crc32@3.0.0': resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} @@ -2488,6 +2533,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-assertions@7.26.0': + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-jsx@7.25.9': resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} engines: {node: '>=6.9.0'} @@ -2780,6 +2831,18 @@ packages: '@emotion/unitless@0.8.0': resolution: {integrity: sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==} + '@envelop/core@5.2.3': + resolution: {integrity: sha512-KfoGlYD/XXQSc3BkM1/k15+JQbkQ4ateHazeZoWl9P71FsLTDXSjGy6j7QqfhpIDSbxNISqhPMfZHYSbDFOofQ==} + engines: {node: '>=18.0.0'} + + '@envelop/instrumentation@1.0.0': + resolution: {integrity: sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==} + engines: {node: '>=18.0.0'} + + '@envelop/types@5.2.1': + resolution: {integrity: sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==} + engines: {node: '>=18.0.0'} + '@esbuild-plugins/node-resolve@0.2.2': resolution: {integrity: sha512-+t5FdX3ATQlb53UFDBRb4nqjYBz492bIrnVWvpQHpzZlu9BQL5HasMZhqc409ygUwOWCXZhrWr6NyZ6T6Y+cxw==} peerDependencies: @@ -3116,6 +3179,9 @@ packages: '@fastify/ajv-compiler@3.6.0': resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==} + '@fastify/busboy@3.1.1': + resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==} + '@fastify/cors@9.0.1': resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==} @@ -3161,6 +3227,22 @@ packages: react: ^16.8.0 || ^17 || ^18 react-dom: ^16.8.0 || ^17 || ^18 + '@graphiql/react@0.28.2': + resolution: {integrity: sha512-6PE2Ff9dXpyQMHy3oKzCjT738kY2+wdQ4Xce8+1K+G2yMGZ716Fo0i4vW3S6ErHVI4S/C76gFTQlv/pzxU5ylg==} + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + + '@graphiql/toolkit@0.11.1': + resolution: {integrity: sha512-G02te70/oYYna5UhbH6TXwNxeQyWa+ChlPonUrKwC5Ot9ItraGJ9yUU4sS+YRaA+EvkzNoHG79XcW2k1QaAMiw==} + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + graphql-ws: '>= 4.5.0' + peerDependenciesMeta: + graphql-ws: + optional: true + '@graphiql/toolkit@0.9.1': resolution: {integrity: sha512-LVt9pdk0830so50ZnU2Znb2rclcoWznG8r8asqAENzV0U1FM1kuY0sdPpc/rBc9MmmNgnB6A+WZzDhq6dbhTHA==} peerDependencies: @@ -3170,6 +3252,242 @@ packages: graphql-ws: optional: true + '@graphql-codegen/add@5.0.3': + resolution: {integrity: sha512-SxXPmramkth8XtBlAHu4H4jYcYXM/o3p01+psU+0NADQowA8jtYkK6MW5rV6T+CxkEaNZItfSmZRPgIuypcqnA==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/cli@5.0.5': + resolution: {integrity: sha512-9p9SI5dPhJdyU+O6p1LUqi5ajDwpm6pUhutb1fBONd0GZltLFwkgWFiFtM6smxkYXlYVzw61p1kTtwqsuXO16w==} + engines: {node: '>=16'} + hasBin: true + peerDependencies: + '@parcel/watcher': ^2.1.0 + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + '@parcel/watcher': + optional: true + + '@graphql-codegen/client-preset@4.8.0': + resolution: {integrity: sha512-IVtTl7GsPMbQihk5+l5fDYksnPPOoC52sKxzquyIyuecZLEB7W3nNLV29r6+y+tjXTRPA774FR7CHGA2adzhjw==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-sock: ^1.0.0 + + '@graphql-codegen/core@4.0.2': + resolution: {integrity: sha512-IZbpkhwVqgizcjNiaVzNAzm/xbWT6YnGgeOLwVjm4KbJn3V2jchVtuzHH09G5/WkkLSk2wgbXNdwjM41JxO6Eg==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/gql-tag-operations@4.0.17': + resolution: {integrity: sha512-2pnvPdIG6W9OuxkrEZ6hvZd142+O3B13lvhrZ48yyEBh2ujtmKokw0eTwDHtlXUqjVS0I3q7+HB2y12G/m69CA==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/plugin-helpers@5.1.0': + resolution: {integrity: sha512-Y7cwEAkprbTKzVIe436TIw4w03jorsMruvCvu0HJkavaKMQbWY+lQ1RIuROgszDbxAyM35twB5/sUvYG5oW+yg==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/schema-ast@4.1.0': + resolution: {integrity: sha512-kZVn0z+th9SvqxfKYgztA6PM7mhnSZaj4fiuBWvMTqA+QqQ9BBed6Pz41KuD/jr0gJtnlr2A4++/0VlpVbCTmQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/typed-document-node@5.1.1': + resolution: {integrity: sha512-Bp/BrMZDKRwzuVeLv+pSljneqONM7gqu57ZaV34Jbncu2hZWMRDMfizTKghoEwwZbRCYYfJO9tA0sYVVIfI1kg==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/typescript-operations@4.6.0': + resolution: {integrity: sha512-/EltSdE/uPoEAblRTVLABVDhsrE//Kl3pCflyG1PWl4gWL9/OzQXYGjo6TF6bPMVn/QBWoO0FeboWf+bk84SXA==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-sock: ^1.0.0 + + '@graphql-codegen/typescript-resolvers@4.5.0': + resolution: {integrity: sha512-+zLe+xzATHTfi4NG7FyM+RiTzFmDqh23McauCnJ9w8n11PMtUHfneu54iryJrJO3u/exzpwkQFOZcIxRYEI4Tg==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-sock: ^1.0.0 + + '@graphql-codegen/typescript@4.1.6': + resolution: {integrity: sha512-vpw3sfwf9A7S+kIUjyFxuvrywGxd4lmwmyYnnDVjVE4kSQ6Td3DpqaPTy8aNQ6O96vFoi/bxbZS2BW49PwSUUA==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/visitor-plugin-common@5.8.0': + resolution: {integrity: sha512-lC1E1Kmuzi3WZUlYlqB4fP6+CvbKH9J+haU1iWmgsBx5/sO2ROeXJG4Dmt8gP03bI2BwjiwV5WxCEMlyeuzLnA==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-hive/signal@1.0.0': + resolution: {integrity: sha512-RiwLMc89lTjvyLEivZ/qxAC5nBHoS2CtsWFSOsN35sxG9zoo5Z+JsFHM8MlvmO9yt+MJNIyC5MLE1rsbOphlag==} + engines: {node: '>=18.0.0'} + + '@graphql-tools/apollo-engine-loader@8.0.20': + resolution: {integrity: sha512-m5k9nXSyjq31yNsEqDXLyykEjjn3K3Mo73oOKI+Xjy8cpnsgbT4myeUJIYYQdLrp7fr9Y9p7ZgwT5YcnwmnAbA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/batch-execute@9.0.15': + resolution: {integrity: sha512-qlWUl6yi87FU5WvyJ0uD81R4Y30oQIuW3mJCjOrEvifyT+f/rEqSZFOhYrofYoZAoTcwqOhy6WgH+b9+AtRYjA==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/code-file-loader@8.1.20': + resolution: {integrity: sha512-GzIbjjWJIc04KWnEr8VKuPe0FA2vDTlkaeub5p4lLimljnJ6C0QSkOyCUnFmsB9jetQcHm0Wfmn/akMnFUG+wA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/delegate@10.2.17': + resolution: {integrity: sha512-z+LpZrTQCEXA4fbdJcSsvhaMqT4xi/O8B0mP30ENGyTbSfa20QamOQx9jgCiw2ii/ucwxfGMhygwlpZG36EU4w==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/documents@1.0.1': + resolution: {integrity: sha512-aweoMH15wNJ8g7b2r4C4WRuJxZ0ca8HtNO54rkye/3duxTkW4fGBEutCx03jCIr5+a1l+4vFJNP859QnAVBVCA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-common@0.0.4': + resolution: {integrity: sha512-SEH/OWR+sHbknqZyROCFHcRrbZeUAyjCsgpVWCRjqjqRbiJiXq6TxNIIOmpXgkrXWW/2Ev4Wms6YSGJXjdCs6Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-graphql-ws@2.0.5': + resolution: {integrity: sha512-gI/D9VUzI1Jt1G28GYpvm5ckupgJ5O8mi5Y657UyuUozX34ErfVdZ81g6oVcKFQZ60LhCzk7jJeykK48gaLhDw==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-http@1.3.3': + resolution: {integrity: sha512-LIy+l08/Ivl8f8sMiHW2ebyck59JzyzO/yF9SFS4NH6MJZUezA1xThUXCDIKhHiD56h/gPojbkpcFvM2CbNE7A==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-legacy-ws@1.1.17': + resolution: {integrity: sha512-TvltY6eL4DY1Vt66Z8kt9jVmNcI+WkvVPQZrPbMCM3rv2Jw/sWvSwzUBezRuWX0sIckMifYVh23VPcGBUKX/wg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor@1.4.7': + resolution: {integrity: sha512-U0nK9jzJRP9/9Izf1+0Gggd6K6RNRsheFo1gC/VWzfnsr0qjcOSS9qTjY0OTC5iTPt4tQ+W5Zpw/uc7mebI6aA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/git-loader@8.0.24': + resolution: {integrity: sha512-ypLC9N2bKNC0QNbrEBTbWKwbV607f7vK2rSGi9uFeGr8E29tWplo6or9V/+TM0ZfIkUsNp/4QX/zKTgo8SbwQg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/github-loader@8.0.20': + resolution: {integrity: sha512-Icch8bKZ1iP3zXCB9I0ded1hda9NPskSSalw7ZM21kXvLiOR5nZhdqPF65gCFkIKo+O4NR4Bp51MkKj+wl+vpg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/graphql-file-loader@8.0.19': + resolution: {integrity: sha512-kyEZL4rRJ5LelfCXL3GLgbMiu5Zd7memZaL8ZxPXGI7DA8On1e5IVBH3zZJwf7LzhjSVnPaHM7O/bRzGvTbXzQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/graphql-tag-pluck@8.3.19': + resolution: {integrity: sha512-LEw/6IYOUz48HjbWntZXDCzSXsOIM1AyWZrlLoJOrA8QAlhFd8h5Tny7opCypj8FO9VvpPFugWoNDh5InPOEQA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/import@7.0.18': + resolution: {integrity: sha512-1tw1/1QLB0n5bPWfIrhCRnrHIlbMvbwuifDc98g4FPhJ7OXD+iUQe+IpmD5KHVwYWXWhZOuJuq45DfV/WLNq3A==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/json-file-loader@8.0.18': + resolution: {integrity: sha512-JjjIxxewgk8HeMR3npR3YbOkB7fxmdgmqB9kZLWdkRKBxrRXVzhryyq+mhmI0Evzt6pNoHIc3vqwmSctG2sddg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/load@8.1.0': + resolution: {integrity: sha512-OGfOm09VyXdNGJS/rLqZ6ztCiG2g6AMxhwtET8GZXTbnjptFc17GtKwJ3Jv5w7mjJ8dn0BHydvIuEKEUK4ciYw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/merge@9.0.24': + resolution: {integrity: sha512-NzWx/Afl/1qHT3Nm1bghGG2l4jub28AdvtG11PoUlmjcIjnFBJMv4vqL0qnxWe8A82peWo4/TkVdjJRLXwgGEw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/optimize@2.0.0': + resolution: {integrity: sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/prisma-loader@8.0.17': + resolution: {integrity: sha512-fnuTLeQhqRbA156pAyzJYN0KxCjKYRU5bz1q/SKOwElSnAU4k7/G1kyVsWLh7fneY78LoMNH5n+KlFV8iQlnyg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/relay-operation-optimizer@7.0.19': + resolution: {integrity: sha512-xnjLpfzw63yIX1bo+BVh4j1attSwqEkUbpJ+HAhdiSUa3FOQFfpWgijRju+3i87CwhjBANqdTZbcsqLT1hEXig==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/schema@10.0.23': + resolution: {integrity: sha512-aEGVpd1PCuGEwqTXCStpEkmheTHNdMayiIKH1xDWqYp9i8yKv9FRDgkGrY4RD8TNxnf7iII+6KOBGaJ3ygH95A==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/url-loader@8.0.31': + resolution: {integrity: sha512-QGP3py6DAdKERHO5D38Oi+6j+v0O3rkBbnLpyOo87rmIRbwE6sOkL5JeHegHs7EEJ279fBX6lMt8ry0wBMGtyA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/utils@10.8.6': + resolution: {integrity: sha512-Alc9Vyg0oOsGhRapfL3xvqh1zV8nKoFUdtLhXX7Ki4nClaIJXckrA86j+uxEuG3ic6j4jlM1nvcWXRn/71AVLQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/wrap@10.0.35': + resolution: {integrity: sha512-qBga3wo7+GqY+ClGexiyRz9xgy1RWozZryTuGX8usGWPa4wKi/tJS4rKWQQesgB3Fh//SZUCRA5u2nwZaZQw1Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@gregnr/postgres-meta@0.82.0-dev.2': resolution: {integrity: sha512-RLCMcshNBZi1FBA60PfQTLCvfuYmNXVzRe147j5mlOIXvq+o9K/pJgm49GOEBBYfvH+bwcs3w/mBRCh7BNveng==} engines: {node: '>=20', npm: '>=9'} @@ -5448,6 +5766,9 @@ packages: resolution: {integrity: sha512-Lzea25WqwxVK5+aCiq/pr7lUFdsZPYSqNzl05Z4jEtuP1DEIxJNG31ID75dZt30pPtyxjaa/dBuccruYlYflzw==} engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@repeaterjs/repeater@3.0.6': + resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} + '@resvg/resvg-wasm@2.4.0': resolution: {integrity: sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==} engines: {node: '>= 10'} @@ -6366,6 +6687,9 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/d3-array@3.0.8': resolution: {integrity: sha512-2xAVyAUgaXHX9fubjcCbGAUOqYfRJN1em1EKR2HfzWBpObZhwfnZKvofTN4TplMqJdFQao61I+NVSai/vnBvDQ==} @@ -6510,6 +6834,9 @@ packages: '@types/geojson@7946.0.12': resolution: {integrity: sha512-uK2z1ZHJyC0nQRbuovXFt4mzXDwf27vQeUWNhfKGwRcWW429GOhP8HxUHlM6TLH4bzmlv/HlEjpvJh3JfmGsAA==} + '@types/graphql-validation-complexity@0.4.4': + resolution: {integrity: sha512-/0AmF5HYHmLErXRky/be86E4hAVO2LE5ejHdy3kf1rASgzBDG3Yb1yqoHxrZz+Nk72jt2UWCfa6DSfdpHDkuzg==} + '@types/hast@2.3.6': resolution: {integrity: sha512-47rJE80oqPmFdVDCD7IheXBrVdwuBgsYwoczFvKmwfo2Mzsnt+V9OONsYauFmICb6lQPpCuXYJWejBNs4pDJRg==} @@ -6552,6 +6879,10 @@ packages: '@types/lodash@4.17.5': resolution: {integrity: sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==} + '@types/lz-string@1.5.0': + resolution: {integrity: sha512-s84fKOrzqqNCAPljhVyC5TjAo6BH4jKHw9NRNFNiRUY5QSgZCmVm5XILlWbisiKl+0OcS7eWihmKGS5akc2iQw==} + deprecated: This is a stub types definition. lz-string provides its own type definitions, so you do not need this installed. + '@types/markdown-table@3.0.0': resolution: {integrity: sha512-vX5zya26rnLLHdAKA3E9ZOT07CJslYp2gZfAZiXKUY5vd2RRiJyOQKrSlmUjyJ+eNoDAPkHflebG3PsfqC32aA==} deprecated: This is a stub types definition. markdown-table provides its own type definitions, so you do not need this installed. @@ -6767,6 +7098,9 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@usercentrics/cmp-browser-sdk@4.42.0': + resolution: {integrity: sha512-/cik01TdeiYUV1+EasK83Ip05TDqmowM5tmWGfRDdaOrneSic5BedBdJHKya4pP2vwiYAtzDYVJe4BwPt2M16g==} + '@vercel/flags@2.6.0': resolution: {integrity: sha512-GvLX0CK/OsIqq672OBvcCeu0k3tb3QdE0lRn1P5zulMQJIPTLlUDEH0y9TeAyC7TMWlpTTT5Bealqu0rmncyXQ==} deprecated: This package was renamed to flags, which offers the same functionality. https://vercel.com/changelog/npm-i-flags @@ -6930,6 +7264,22 @@ packages: '@webgpu/types@0.1.49': resolution: {integrity: sha512-NMmS8/DofhH/IFeW+876XrHVWel+J/vdcFCHLDqeJgkH9x0DeiwjVd8LcBdaxdG/T7Rf8VUAYsA8X1efMzLjRQ==} + '@whatwg-node/disposablestack@0.0.6': + resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/fetch@0.10.6': + resolution: {integrity: sha512-6uzhO2aQ757p3bSHcemA8C4pqEXuyBqyGAM7cYpO0c6/igRMV9As9XL0W12h5EPYMclgr7FgjmbVQBoWEdJ/yA==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/node-fetch@0.7.18': + resolution: {integrity: sha512-IxKdVWfZYasGiyxBcsROxq6FmDQu3MNNiOYJ/yqLKhe+Qq27IIWsK7ItbjS2M9L5aM5JxjWkIS7JDh7wnsn+CQ==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/promise-helpers@1.3.1': + resolution: {integrity: sha512-D+OwTEunoQhVHVToD80dPhfz9xgPLqJyEA3F5jCRM14A2u8tBBQVdZekqfqx6ZAfZ+POT4Hb0dn601UKMsvADw==} + engines: {node: '>=16.0.0'} + '@xobotyi/scrollbar-width@1.9.5': resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} @@ -7223,6 +7573,9 @@ packages: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + assert-plus@1.0.0: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} engines: {node: '>=0.8'} @@ -7238,6 +7591,10 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + astring@1.8.6: resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} hasBin: true @@ -7259,6 +7616,10 @@ packages: resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} engines: {node: '>=4'} + auto-bind@4.0.0: + resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} + engines: {node: '>=8'} + autolinker@0.28.1: resolution: {integrity: sha512-zQAFO1Dlsn69eXaO6+7YZc+v84aquQKbwpzCE3L0stj56ERn9hutFxPopViLjo9G+rWwjozRhgS5KJ25Xy19cQ==} @@ -7351,6 +7712,9 @@ packages: bintrees@1.0.2: resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bl@5.1.0: resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} @@ -7384,6 +7748,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-crc32@1.0.0: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} @@ -7395,6 +7762,9 @@ packages: resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} engines: {node: '>=4'} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -7491,6 +7861,9 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case-all@1.0.15: + resolution: {integrity: sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ==} + change-case@4.1.2: resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} @@ -7518,6 +7891,9 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -7582,6 +7958,10 @@ packages: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + cli-cursor@4.0.0: resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -7590,6 +7970,14 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -7661,6 +8049,13 @@ packages: codemirror: ^5.65.3 graphql: ^15.5.0 || ^16.0.0 + codemirror-graphql@2.2.0: + resolution: {integrity: sha512-egIiewf5zEH5LLSkJpJDpYxO1OkNruD0gTWiBrS1JmXk7yjt5WPw7jSmDRkWJx8JheHONltaJPNPWdTUT5LRIQ==} + peerDependencies: + '@codemirror/language': 6.0.0 + codemirror: ^5.65.3 + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + codemirror@5.65.15: resolution: {integrity: sha512-YC4EHbbwQeubZzxLl5G4nlbLc1T21QTrKGaOal/Pkm9dVDMZXMH7+ieSPEOZCtO9I68i8/oteJKOxzHC2zR+0g==} @@ -7849,6 +8244,15 @@ packages: typescript: optional: true + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -7876,6 +8280,13 @@ packages: resolution: {integrity: sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg==} hasBin: true + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + cross-inspect@1.0.1: + resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==} + engines: {node: '>=16.0.0'} + cross-spawn@6.0.6: resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} engines: {node: '>=4.8'} @@ -8051,6 +8462,9 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + dataloader@2.2.3: + resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} + date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -8208,6 +8622,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dependency-graph@0.11.0: + resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} + engines: {node: '>= 0.6.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -8224,6 +8642,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} @@ -8346,6 +8768,10 @@ packages: resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} engines: {node: '>=4'} + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -8353,6 +8779,9 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + dynamic-import-polyfill@0.1.1: + resolution: {integrity: sha512-m953zv0w5oDagTItWm6Auhmk/pY7EiejaqiVbnzSS3HIjh1FCUeK7WzuaVtWPNs58A+/xpIE+/dVk6pKsrua8g==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -8858,6 +9287,10 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + extsprintf@1.4.1: resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} engines: {'0': node >=0.6.0} @@ -8954,6 +9387,15 @@ packages: fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fbjs-css-vars@1.0.2: + resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} + + fbjs@3.0.5: + resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + fdir@6.4.3: resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} peerDependencies: @@ -8972,6 +9414,10 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -9205,6 +9651,10 @@ packages: get-tsconfig@4.7.2: resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + get-value@3.0.1: + resolution: {integrity: sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==} + engines: {node: '>=6.0'} + giget@2.0.0: resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} hasBin: true @@ -9294,18 +9744,83 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphiql@3.8.3: + resolution: {integrity: sha512-cuPDYtXVKV86Pu5PHBX642Odi/uVEE2y1Jxq5rGO/Qy1G2lRp7ZZ7a/T30RzxhuLSWo9zUbzq0P3U49//H0Ugw==} + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + + graphql-config@5.1.4: + resolution: {integrity: sha512-ObdBeL3ycddHrNbFvhGZ12pK8jUzWvvyN2A+6ij3XrtLH/KrkXt+BboEAEgXmeUrTcD5RjJnz8IZu3Cgc/oX/w==} + engines: {node: '>= 16.0.0'} + peerDependencies: + cosmiconfig-toml-loader: ^1.0.0 + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + cosmiconfig-toml-loader: + optional: true + graphql-language-service@5.2.0: resolution: {integrity: sha512-o/ZgTS0pBxWm3hSF4+6GwiV1//DxzoLWEbS38+jqpzzy1d/QXBidwQuVYTOksclbtOJZ3KR/tZ8fi/tI6VpVMg==} hasBin: true peerDependencies: graphql: ^15.5.0 || ^16.0.0 + graphql-language-service@5.3.0: + resolution: {integrity: sha512-gCQIIy7lM9CB1KPLEb+DNZLczA9zuTLEOJE2hEQZTFYInogdmMDRa6RAkvM4LL0LcgcS+3uPs6KtHlcjCqRbUg==} + hasBin: true + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2 + + graphql-request@6.1.0: + resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} + peerDependencies: + graphql: 14 - 16 + + graphql-sock@1.0.1: + resolution: {integrity: sha512-gSA0CXdNMvNlpEnH2GY1//SUY7laDsAn51sDm4yh6TTH5UkfbNINydyUAoMHHkAaCaOLNXELQmu3GVcSOw4twg==} + hasBin: true + peerDependencies: + graphql: 15.x || 16.x || 17.x + + graphql-tag@2.12.6: + resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + graphql-validation-complexity@0.4.2: + resolution: {integrity: sha512-4tzmN/70a06c2JH5fvISkoLX6oBDpqK22cvr2comge3HZHtBLD3n5Sl6MnQYMVhQqKGlpZWcCgD00MnyKNzYYg==} + peerDependencies: + graphql: '>=0.9.5' + graphql-ws@5.14.1: resolution: {integrity: sha512-aqkls1espsygP1PfkAuuLIV96IbztQ6EaADse97pw8wRIMT3+AL/OYfS8V2iCRkc0gzckitoDRGCQEdnySggiA==} engines: {node: '>=10'} peerDependencies: graphql: '>=0.11 <=16' + graphql-ws@6.0.4: + resolution: {integrity: sha512-8b4OZtNOvv8+NZva8HXamrc0y1jluYC0+13gdh7198FKjVzXyTvVc95DCwGzaKEfn3YuWZxUqjJlHe3qKM/F2g==} + engines: {node: '>=20'} + peerDependencies: + '@fastify/websocket': ^10 || ^11 + graphql: ^15.10.1 || ^16 + uWebSockets.js: ^20 + ws: ^8 + peerDependenciesMeta: + '@fastify/websocket': + optional: true + uWebSockets.js: + optional: true + ws: + optional: true + + graphql@16.10.0: + resolution: {integrity: sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + graphql@16.8.1: resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -9585,6 +10100,10 @@ packages: hyphenate-style-name@1.0.4: resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -9610,6 +10129,10 @@ packages: immutability-helper@3.1.1: resolution: {integrity: sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==} + immutable@3.7.6: + resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==} + engines: {node: '>=0.8.0'} + immutable@4.3.4: resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} @@ -9617,6 +10140,10 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} + import-from@4.0.0: + resolution: {integrity: sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==} + engines: {node: '>=12.2'} + import-in-the-middle@1.13.1: resolution: {integrity: sha512-k2V9wNm9B+ysuelDTHjI9d5KPc4l8zAZTGqj+pcynvWkypZd857ryzN8jNC7Pg2YZXNMJcHRPpaDyCBbNyVRpA==} @@ -9667,6 +10194,10 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 + inquirer@8.2.6: + resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} + engines: {node: '>=12.0.0'} + internal-slot@1.0.7: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} @@ -9678,6 +10209,9 @@ packages: intersection-observer@0.10.0: resolution: {integrity: sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ==} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ioredis@5.6.0: resolution: {integrity: sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==} engines: {node: '>=12.22.0'} @@ -9696,6 +10230,10 @@ packages: iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-absolute@1.0.0: + resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==} + engines: {node: '>=0.10.0'} + is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} @@ -9813,6 +10351,10 @@ packages: engines: {node: '>=14.16'} hasBin: true + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + is-interactive@2.0.0: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} @@ -9820,6 +10362,9 @@ packages: is-lambda@1.0.1: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + is-lower-case@2.0.2: + resolution: {integrity: sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==} + is-map@2.0.2: resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} @@ -9890,6 +10435,10 @@ packages: resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} engines: {node: '>=12'} + is-relative@1.0.0: + resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==} + engines: {node: '>=0.10.0'} + is-set@2.0.2: resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} @@ -9917,10 +10466,21 @@ packages: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} + is-unc-path@1.0.0: + resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==} + engines: {node: '>=0.10.0'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} + is-upper-case@2.0.2: + resolution: {integrity: sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==} + is-weakmap@2.0.1: resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} @@ -9934,6 +10494,10 @@ packages: resolution: {integrity: sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==} engines: {node: '>=12.13'} + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -9975,6 +10539,11 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -10153,6 +10722,10 @@ packages: json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json-to-pretty-yaml@1.2.2: + resolution: {integrity: sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==} + engines: {node: '>= 0.2.0'} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -10272,6 +10845,9 @@ packages: linkify-it@3.0.3: resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + list-item@1.1.1: resolution: {integrity: sha512-S3D0WZ4J6hyM8o5SNKWaMYB1ALSacPZ2nHGEuCjmHZ+dc03gFeNZoNDcqfcnO4vDhTZmNrqrpYZCdXsRh22bzw==} engines: {node: '>=0.10.0'} @@ -10280,6 +10856,15 @@ packages: resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==} hasBin: true + listr2@4.0.5: + resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} + engines: {node: '>=12'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + load-json-file@4.0.0: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} @@ -10349,6 +10934,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash.template@4.5.0: resolution: {integrity: sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==} deprecated: This package is deprecated. Use https://socket.dev/npm/package/eta instead. @@ -10365,10 +10953,18 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + log-symbols@5.1.0: resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} engines: {node: '>=12'} + log-update@4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} @@ -10385,6 +10981,9 @@ packages: loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + lower-case-first@2.0.2: + resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -10454,6 +11053,10 @@ packages: resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} engines: {node: '>= 10'} + map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} @@ -10469,6 +11072,10 @@ packages: resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==} hasBin: true + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + markdown-link@0.1.1: resolution: {integrity: sha512-TurLymbyLyo+kAUUAV9ggR9EPcDjP/ctlv9QAFiqUH7c+t6FlsbivPo9OKTU8xdOx9oNd2drW/Fi5RRElQbUqA==} engines: {node: '>=0.10.0'} @@ -10617,6 +11224,9 @@ packages: mdurl@1.0.1: resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + mdx-bundler@10.0.2: resolution: {integrity: sha512-0wF0zoCv+Ms4G+eSlk/jaKYoJHc0oXBaOma3kYlFJiKq9H8h41Dd66ioDBGF4noy80Pf7KTBQlyHfEpTqVml7A==} engines: {node: '>=18', npm: '>=6'} @@ -11111,6 +11721,9 @@ packages: typescript: optional: true + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -11305,6 +11918,9 @@ packages: engines: {node: '>= 10.12.0'} hasBin: true + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + node-mock-http@1.0.0: resolution: {integrity: sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==} @@ -11346,6 +11962,10 @@ packages: resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -11583,10 +12203,18 @@ packages: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + ora@6.3.1: resolution: {integrity: sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + outdent@0.8.0: resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} @@ -11636,6 +12264,10 @@ packages: parse-entities@4.0.1: resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} + parse-filepath@1.0.2: + resolution: {integrity: sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==} + engines: {node: '>=0.8'} + parse-json@4.0.0: resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} engines: {node: '>=4'} @@ -11696,6 +12328,14 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-root-regex@0.1.2: + resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} + engines: {node: '>=0.10.0'} + + path-root@0.1.1: + resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} + engines: {node: '>=0.10.0'} + path-scurry@1.11.1: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} @@ -12098,6 +12738,9 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} + promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -12137,6 +12780,10 @@ packages: psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -12209,6 +12856,11 @@ packages: react: ^16.8.5 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-compiler-runtime@19.0.0-beta-37ed2a7-20241206: + resolution: {integrity: sha512-9e6rCpVylr9EnVocgYAjft7+2v01BDpajeHKRoO+oc9pKcAMTpstHtHvE/TSVbyf4FvzCGjfKcfHM9XGTXI6Tw==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-contexify@5.0.0: resolution: {integrity: sha512-2FIp7lxJ6dtfGr8EZ4uVV5p5TQjd0n2h/JU7PrejNIMiCeZWvSVPFh4lj1ZvjXosglBvP7q5JQQ8yUCdSaMSaw==} engines: {node: '>=10'} @@ -12426,8 +13078,8 @@ packages: peerDependencies: react: '>= 16.3' - react-router@7.4.0: - resolution: {integrity: sha512-Y2g5ObjkvX3VFeVt+0CIPuYd9PpgqCslG7ASSIdN73LwA1nNWzcMLaoMRJfP3prZFI92svxFwbn7XkLJ+UPQ6A==} + react-router@7.5.3: + resolution: {integrity: sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -12652,6 +13304,9 @@ packages: rehype-stringify@10.0.0: resolution: {integrity: sha512-1TX1i048LooI9QoecrXy7nGFFbFSufxVRAfc6Y9YMRAi56l+oB0zP51mLSV312uRuvVLPV1opSlJmslozR1XHQ==} + relay-runtime@12.0.0: + resolution: {integrity: sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==} + remark-code-import@1.2.0: resolution: {integrity: sha512-fgwLruqlZbVOIhCJFjY+JDwPZhA4/eK3InJzN8Ox8UDdtudpG212JwtRj6la+lAzJU7JmSEyewZSukVZdknt3Q==} engines: {node: '>= 12'} @@ -12713,9 +13368,18 @@ packages: engines: {node: '>= 0.10.0'} hasBin: true + remedial@1.0.8: + resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==} + remove-accents@0.4.2: resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} + remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + + remove-trailing-spaces@1.0.9: + resolution: {integrity: sha512-xzG7w5IRijvIkHIjDk65URsJJ7k4J95wmcArY5PRcmjldIOl7oTvG8+X2Ag690R7SfwiOcHrWZKVc1Pp5WIOzA==} + rename-keys@1.2.0: resolution: {integrity: sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==} engines: {node: '>= 0.8.0'} @@ -12774,6 +13438,10 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + restore-cursor@4.0.0: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -12850,6 +13518,10 @@ packages: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} engines: {node: '>=18'} + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -12915,6 +13587,9 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + scuid@1.1.0: + resolution: {integrity: sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==} + scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} @@ -12989,6 +13664,9 @@ packages: resolution: {integrity: sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==} engines: {node: '>=11.0'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -13063,6 +13741,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + signedsource@1.0.0: + resolution: {integrity: sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==} + simple-git@3.24.0: resolution: {integrity: sha512-QqAKee9Twv+3k8IFOFfPB2hnk6as6Y6ACUpwCtQvRYBAes23Wv3SZlHVobAzqcE8gfsisCvPw3HGW3HYM+VYYw==} @@ -13095,6 +13776,14 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + slugify@1.4.7: resolution: {integrity: sha512-tf+h5W1IrjNm/9rKKj0JU2MDMruiopx0jjVA5zCdBtcGjfp0+c5rHw/zADLC3IeKlGHtVbHtpfzvYA0OYT+HKg==} engines: {node: '>=8.0.0'} @@ -13190,6 +13879,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sponge-case@1.0.1: + resolution: {integrity: sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -13289,6 +13981,9 @@ packages: strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-env-interpolation@1.0.1: + resolution: {integrity: sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -13485,6 +14180,9 @@ packages: resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} hasBin: true + swap-case@2.0.2: + resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==} + swiper@11.0.7: resolution: {integrity: sha512-cDfglW1B6uSmB6eB6pNmzDTNLmZtu5bWWa1vak0RU7fOI9qHjMzl7gVBvYSl34b0RU2N11HxxETJqQ5LeqI1cA==} engines: {node: '>= 4.7.0'} @@ -13505,6 +14203,10 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + sync-fetch@0.6.0-2: + resolution: {integrity: sha512-c7AfkZ9udatCuAy9RSfiGPpeOKKUAUK5e1cXadLOGUjasdxqYqAK0jTNkM/FSEyJ3a5Ra27j/tw/PS0qLmaF/A==} + engines: {node: '>=18'} + system-architecture@0.1.0: resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} engines: {node: '>=18'} @@ -13607,6 +14309,13 @@ packages: through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + timeout-signal@2.0.0: + resolution: {integrity: sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==} + engines: {node: '>=16'} + tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} @@ -13648,6 +14357,13 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + title-case@3.0.3: + resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -13733,6 +14449,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-log@2.2.7: + resolution: {integrity: sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==} + ts-morph@18.0.0: resolution: {integrity: sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==} @@ -13900,9 +14619,16 @@ packages: engines: {node: '>=14.17'} hasBin: true + ua-parser-js@1.0.40: + resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==} + hasBin: true + uc.micro@1.0.6: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} @@ -13920,6 +14646,10 @@ packages: unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + unc-path-regex@0.1.2: + resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} + engines: {node: '>=0.10.0'} + uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} @@ -14047,6 +14777,10 @@ packages: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} + unixify@1.0.0: + resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} + engines: {node: '>=0.10.0'} + unplugin-utils@0.2.4: resolution: {integrity: sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==} engines: {node: '>=18.12.0'} @@ -14165,6 +14899,9 @@ packages: url-template@2.0.8: resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} + urlpattern-polyfill@10.0.0: + resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -14465,6 +15202,10 @@ packages: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + whatwg-url@11.0.0: resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} engines: {node: '>=12'} @@ -14768,6 +15509,22 @@ snapshots: '@antfu/ni@23.3.1': {} + '@ardatan/relay-compiler@12.0.3(encoding@0.1.13)(graphql@16.10.0)': + dependencies: + '@babel/generator': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/runtime': 7.26.10 + chalk: 4.1.2 + fb-watchman: 2.0.2 + graphql: 16.10.0 + immutable: 3.7.6 + invariant: 2.2.4 + nullthrows: 1.1.1 + relay-runtime: 12.0.0(encoding@0.1.13) + signedsource: 1.0.0 + transitivePeerDependencies: + - encoding + '@aws-crypto/crc32@3.0.0': dependencies: '@aws-crypto/util': 3.0.0 @@ -15301,6 +16058,11 @@ snapshots: '@babel/core': 7.26.10(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.26.5 + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.10(supports-color@8.1.1))': + dependencies: + '@babel/core': 7.26.10(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.26.5 + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.10(supports-color@8.1.1))': dependencies: '@babel/core': 7.26.10(supports-color@8.1.1) @@ -15827,6 +16589,23 @@ snapshots: '@emotion/unitless@0.8.0': {} + '@envelop/core@5.2.3': + dependencies: + '@envelop/instrumentation': 1.0.0 + '@envelop/types': 5.2.1 + '@whatwg-node/promise-helpers': 1.3.1 + tslib: 2.8.1 + + '@envelop/instrumentation@1.0.0': + dependencies: + '@whatwg-node/promise-helpers': 1.3.1 + tslib: 2.8.1 + + '@envelop/types@5.2.1': + dependencies: + '@whatwg-node/promise-helpers': 1.3.1 + tslib: 2.8.1 + '@esbuild-plugins/node-resolve@0.2.2(esbuild@0.25.2)(supports-color@8.1.1)': dependencies: '@types/resolve': 1.20.6 @@ -16022,6 +16801,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.12.0) fast-uri: 2.4.0 + '@fastify/busboy@3.1.1': {} + '@fastify/cors@9.0.1': dependencies: fastify-plugin: 4.5.1 @@ -16070,9 +16851,9 @@ snapshots: '@gar/promisify@1.1.3': {} - '@graphiql/react@0.19.4(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.8.1))(graphql@16.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@graphiql/react@0.19.4(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@graphiql/toolkit': 0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.8.1))(graphql@16.8.1) + '@graphiql/toolkit': 0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0) '@headlessui/react': 1.7.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -16081,11 +16862,11 @@ snapshots: '@types/codemirror': 5.60.10 clsx: 1.2.1 codemirror: 5.65.15 - codemirror-graphql: 2.0.10(@codemirror/language@6.11.0)(codemirror@5.65.15)(graphql@16.8.1) + codemirror-graphql: 2.0.10(@codemirror/language@6.11.0)(codemirror@5.65.15)(graphql@16.10.0) copy-to-clipboard: 3.3.3 framer-motion: 6.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - graphql: 16.8.1 - graphql-language-service: 5.2.0(graphql@16.8.1) + graphql: 16.10.0 + graphql-language-service: 5.2.0(graphql@16.10.0) markdown-it: 12.3.2 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -16097,16 +16878,520 @@ snapshots: - '@types/react-dom' - graphql-ws - '@graphiql/toolkit@0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.8.1))(graphql@16.8.1)': + '@graphiql/react@0.28.2(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@graphiql/toolkit': 0.11.1(@types/node@20.12.11)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0) + '@headlessui/react': 1.7.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': 1.1.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/codemirror': 5.60.10 + clsx: 1.2.1 + codemirror: 5.65.15 + codemirror-graphql: 2.2.0(@codemirror/language@6.11.0)(codemirror@5.65.15)(graphql@16.10.0) + copy-to-clipboard: 3.3.3 + framer-motion: 6.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + get-value: 3.0.1 + graphql: 16.10.0 + graphql-language-service: 5.3.0(graphql@16.10.0) + markdown-it: 14.1.0 + react: 18.3.1 + react-compiler-runtime: 19.0.0-beta-37ed2a7-20241206(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + set-value: 4.1.0 + transitivePeerDependencies: + - '@codemirror/language' + - '@types/node' + - '@types/react' + - '@types/react-dom' + - graphql-ws + + '@graphiql/toolkit@0.11.1(@types/node@20.12.11)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0)': dependencies: '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 - graphql: 16.8.1 + graphql: 16.10.0 meros: 1.3.0(@types/node@20.12.11) optionalDependencies: - graphql-ws: 5.14.1(graphql@16.8.1) + graphql-ws: 6.0.4(graphql@16.10.0)(ws@8.18.1) transitivePeerDependencies: - '@types/node' + '@graphiql/toolkit@0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)': + dependencies: + '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 + graphql: 16.10.0 + meros: 1.3.0(@types/node@20.12.11) + optionalDependencies: + graphql-ws: 5.14.1(graphql@16.10.0) + transitivePeerDependencies: + - '@types/node' + + '@graphiql/toolkit@0.9.1(@types/node@20.12.11)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0)': + dependencies: + '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 + graphql: 16.10.0 + meros: 1.3.0(@types/node@20.12.11) + optionalDependencies: + graphql-ws: 6.0.4(graphql@16.10.0)(ws@8.18.1) + transitivePeerDependencies: + - '@types/node' + + '@graphql-codegen/add@5.0.3(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.6.2 + + '@graphql-codegen/cli@5.0.5(@parcel/watcher@2.5.1)(@types/node@20.12.11)(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0)(supports-color@8.1.1)(typescript@5.5.2)': + dependencies: + '@babel/generator': 7.27.0 + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 + '@graphql-codegen/client-preset': 4.8.0(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0) + '@graphql-codegen/core': 4.0.2(graphql@16.10.0) + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-tools/apollo-engine-loader': 8.0.20(graphql@16.10.0) + '@graphql-tools/code-file-loader': 8.1.20(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/git-loader': 8.0.24(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/github-loader': 8.0.20(@types/node@20.12.11)(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/graphql-file-loader': 8.0.19(graphql@16.10.0) + '@graphql-tools/json-file-loader': 8.0.18(graphql@16.10.0) + '@graphql-tools/load': 8.1.0(graphql@16.10.0) + '@graphql-tools/prisma-loader': 8.0.17(@types/node@20.12.11)(encoding@0.1.13)(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/url-loader': 8.0.31(@types/node@20.12.11)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@whatwg-node/fetch': 0.10.6 + chalk: 4.1.2 + cosmiconfig: 8.3.6(typescript@5.5.2) + debounce: 1.2.1 + detect-indent: 6.1.0 + graphql: 16.10.0 + graphql-config: 5.1.4(@types/node@20.12.11)(graphql@16.10.0)(typescript@5.5.2) + inquirer: 8.2.6 + is-glob: 4.0.3 + jiti: 1.21.7 + json-to-pretty-yaml: 1.2.2 + listr2: 4.0.5 + log-symbols: 4.1.0 + micromatch: 4.0.8 + shell-quote: 1.8.1 + string-env-interpolation: 1.0.1 + ts-log: 2.2.7 + tslib: 2.8.1 + yaml: 2.4.5 + yargs: 17.7.2 + optionalDependencies: + '@parcel/watcher': 2.5.1 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - cosmiconfig-toml-loader + - encoding + - enquirer + - graphql-sock + - supports-color + - typescript + - uWebSockets.js + - utf-8-validate + + '@graphql-codegen/client-preset@4.8.0(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0)': + dependencies: + '@babel/helper-plugin-utils': 7.26.5 + '@babel/template': 7.27.0 + '@graphql-codegen/add': 5.0.3(graphql@16.10.0) + '@graphql-codegen/gql-tag-operations': 4.0.17(encoding@0.1.13)(graphql@16.10.0) + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-codegen/typed-document-node': 5.1.1(encoding@0.1.13)(graphql@16.10.0) + '@graphql-codegen/typescript': 4.1.6(encoding@0.1.13)(graphql@16.10.0) + '@graphql-codegen/typescript-operations': 4.6.0(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(encoding@0.1.13)(graphql@16.10.0) + '@graphql-tools/documents': 1.0.1(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) + graphql: 16.10.0 + graphql-sock: 1.0.1(graphql@16.10.0) + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/core@4.0.2(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-tools/schema': 10.0.23(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.6.2 + + '@graphql-codegen/gql-tag-operations@4.0.17(encoding@0.1.13)(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(encoding@0.1.13)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + auto-bind: 4.0.0 + graphql: 16.10.0 + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/plugin-helpers@5.1.0(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + change-case-all: 1.0.15 + common-tags: 1.8.2 + graphql: 16.10.0 + import-from: 4.0.0 + lodash: 4.17.21 + tslib: 2.6.2 + + '@graphql-codegen/schema-ast@4.1.0(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.6.2 + + '@graphql-codegen/typed-document-node@5.1.1(encoding@0.1.13)(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(encoding@0.1.13)(graphql@16.10.0) + auto-bind: 4.0.0 + change-case-all: 1.0.15 + graphql: 16.10.0 + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/typescript-operations@4.6.0(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-codegen/typescript': 4.1.6(encoding@0.1.13)(graphql@16.10.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(encoding@0.1.13)(graphql@16.10.0) + auto-bind: 4.0.0 + graphql: 16.10.0 + graphql-sock: 1.0.1(graphql@16.10.0) + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/typescript-resolvers@4.5.0(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-codegen/typescript': 4.1.6(encoding@0.1.13)(graphql@16.10.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(encoding@0.1.13)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + auto-bind: 4.0.0 + graphql: 16.10.0 + graphql-sock: 1.0.1(graphql@16.10.0) + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/typescript@4.1.6(encoding@0.1.13)(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-codegen/schema-ast': 4.1.0(graphql@16.10.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(encoding@0.1.13)(graphql@16.10.0) + auto-bind: 4.0.0 + graphql: 16.10.0 + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/visitor-plugin-common@5.8.0(encoding@0.1.13)(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-tools/optimize': 2.0.0(graphql@16.10.0) + '@graphql-tools/relay-operation-optimizer': 7.0.19(encoding@0.1.13)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + auto-bind: 4.0.0 + change-case-all: 1.0.15 + dependency-graph: 0.11.0 + graphql: 16.10.0 + graphql-tag: 2.12.6(graphql@16.10.0) + parse-filepath: 1.0.2 + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-hive/signal@1.0.0': {} + + '@graphql-tools/apollo-engine-loader@8.0.20(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@whatwg-node/fetch': 0.10.6 + graphql: 16.10.0 + sync-fetch: 0.6.0-2 + tslib: 2.8.1 + + '@graphql-tools/batch-execute@9.0.15(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@whatwg-node/promise-helpers': 1.3.1 + dataloader: 2.2.3 + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/code-file-loader@8.1.20(graphql@16.10.0)(supports-color@8.1.1)': + dependencies: + '@graphql-tools/graphql-tag-pluck': 8.3.19(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + globby: 11.1.0 + graphql: 16.10.0 + tslib: 2.8.1 + unixify: 1.0.0 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/delegate@10.2.17(graphql@16.10.0)': + dependencies: + '@graphql-tools/batch-execute': 9.0.15(graphql@16.10.0) + '@graphql-tools/executor': 1.4.7(graphql@16.10.0) + '@graphql-tools/schema': 10.0.23(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/promise-helpers': 1.3.1 + dataloader: 2.2.3 + dset: 3.1.4 + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/documents@1.0.1(graphql@16.10.0)': + dependencies: + graphql: 16.10.0 + lodash.sortby: 4.7.0 + tslib: 2.8.1 + + '@graphql-tools/executor-common@0.0.4(graphql@16.10.0)': + dependencies: + '@envelop/core': 5.2.3 + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + + '@graphql-tools/executor-graphql-ws@2.0.5(graphql@16.10.0)': + dependencies: + '@graphql-tools/executor-common': 0.0.4(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@whatwg-node/disposablestack': 0.0.6 + graphql: 16.10.0 + graphql-ws: 6.0.4(graphql@16.10.0)(ws@8.18.1) + isomorphic-ws: 5.0.0(ws@8.18.1) + tslib: 2.8.1 + ws: 8.18.1 + transitivePeerDependencies: + - '@fastify/websocket' + - bufferutil + - uWebSockets.js + - utf-8-validate + + '@graphql-tools/executor-http@1.3.3(@types/node@20.12.11)(graphql@16.10.0)': + dependencies: + '@graphql-hive/signal': 1.0.0 + '@graphql-tools/executor-common': 0.0.4(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/fetch': 0.10.6 + '@whatwg-node/promise-helpers': 1.3.1 + graphql: 16.10.0 + meros: 1.3.0(@types/node@20.12.11) + tslib: 2.8.1 + transitivePeerDependencies: + - '@types/node' + + '@graphql-tools/executor-legacy-ws@1.1.17(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@types/ws': 8.5.10 + graphql: 16.10.0 + isomorphic-ws: 5.0.0(ws@8.18.1) + tslib: 2.8.1 + ws: 8.18.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@graphql-tools/executor@1.4.7(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/promise-helpers': 1.3.1 + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/git-loader@8.0.24(graphql@16.10.0)(supports-color@8.1.1)': + dependencies: + '@graphql-tools/graphql-tag-pluck': 8.3.19(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + is-glob: 4.0.3 + micromatch: 4.0.8 + tslib: 2.8.1 + unixify: 1.0.0 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/github-loader@8.0.20(@types/node@20.12.11)(graphql@16.10.0)(supports-color@8.1.1)': + dependencies: + '@graphql-tools/executor-http': 1.3.3(@types/node@20.12.11)(graphql@16.10.0) + '@graphql-tools/graphql-tag-pluck': 8.3.19(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@whatwg-node/fetch': 0.10.6 + '@whatwg-node/promise-helpers': 1.3.1 + graphql: 16.10.0 + sync-fetch: 0.6.0-2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@types/node' + - supports-color + + '@graphql-tools/graphql-file-loader@8.0.19(graphql@16.10.0)': + dependencies: + '@graphql-tools/import': 7.0.18(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + globby: 11.1.0 + graphql: 16.10.0 + tslib: 2.8.1 + unixify: 1.0.0 + + '@graphql-tools/graphql-tag-pluck@8.3.19(graphql@16.10.0)(supports-color@8.1.1)': + dependencies: + '@babel/core': 7.26.10(supports-color@8.1.1) + '@babel/parser': 7.27.0 + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.10(supports-color@8.1.1)) + '@babel/traverse': 7.27.0(supports-color@8.1.1) + '@babel/types': 7.27.0 + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/import@7.0.18(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + resolve-from: 5.0.0 + tslib: 2.8.1 + + '@graphql-tools/json-file-loader@8.0.18(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + globby: 11.1.0 + graphql: 16.10.0 + tslib: 2.8.1 + unixify: 1.0.0 + + '@graphql-tools/load@8.1.0(graphql@16.10.0)': + dependencies: + '@graphql-tools/schema': 10.0.23(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + p-limit: 3.1.0 + tslib: 2.8.1 + + '@graphql-tools/merge@9.0.24(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/optimize@2.0.0(graphql@16.10.0)': + dependencies: + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/prisma-loader@8.0.17(@types/node@20.12.11)(encoding@0.1.13)(graphql@16.10.0)(supports-color@8.1.1)': + dependencies: + '@graphql-tools/url-loader': 8.0.31(@types/node@20.12.11)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@types/js-yaml': 4.0.6 + '@whatwg-node/fetch': 0.10.6 + chalk: 4.1.2 + debug: 4.4.0(supports-color@8.1.1) + dotenv: 16.4.7 + graphql: 16.10.0 + graphql-request: 6.1.0(encoding@0.1.13)(graphql@16.10.0) + http-proxy-agent: 7.0.2(supports-color@8.1.1) + https-proxy-agent: 7.0.6(supports-color@8.1.1) + jose: 5.2.1 + js-yaml: 4.1.0 + lodash: 4.17.21 + scuid: 1.1.0 + tslib: 2.8.1 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - encoding + - supports-color + - uWebSockets.js + - utf-8-validate + + '@graphql-tools/relay-operation-optimizer@7.0.19(encoding@0.1.13)(graphql@16.10.0)': + dependencies: + '@ardatan/relay-compiler': 12.0.3(encoding@0.1.13)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.8.1 + transitivePeerDependencies: + - encoding + + '@graphql-tools/schema@10.0.23(graphql@16.10.0)': + dependencies: + '@graphql-tools/merge': 9.0.24(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/url-loader@8.0.31(@types/node@20.12.11)(graphql@16.10.0)': + dependencies: + '@graphql-tools/executor-graphql-ws': 2.0.5(graphql@16.10.0) + '@graphql-tools/executor-http': 1.3.3(@types/node@20.12.11)(graphql@16.10.0) + '@graphql-tools/executor-legacy-ws': 1.1.17(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@graphql-tools/wrap': 10.0.35(graphql@16.10.0) + '@types/ws': 8.5.10 + '@whatwg-node/fetch': 0.10.6 + '@whatwg-node/promise-helpers': 1.3.1 + graphql: 16.10.0 + isomorphic-ws: 5.0.0(ws@8.18.1) + sync-fetch: 0.6.0-2 + tslib: 2.8.1 + ws: 8.18.1 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - uWebSockets.js + - utf-8-validate + + '@graphql-tools/utils@10.8.6(graphql@16.10.0)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) + '@whatwg-node/promise-helpers': 1.3.1 + cross-inspect: 1.0.1 + dset: 3.1.4 + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/wrap@10.0.35(graphql@16.10.0)': + dependencies: + '@graphql-tools/delegate': 10.2.17(graphql@16.10.0) + '@graphql-tools/schema': 10.0.23(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@whatwg-node/promise-helpers': 1.3.1 + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-typed-document-node/core@3.2.0(graphql@16.10.0)': + dependencies: + graphql: 16.10.0 + '@gregnr/postgres-meta@0.82.0-dev.2(encoding@0.1.13)(supports-color@8.1.1)': dependencies: '@fastify/cors': 9.0.1 @@ -16780,7 +18065,7 @@ snapshots: dependencies: agent-base: 7.1.1(supports-color@8.1.1) http-proxy-agent: 7.0.2(supports-color@8.1.1) - https-proxy-agent: 7.0.4(supports-color@8.1.1) + https-proxy-agent: 7.0.6(supports-color@8.1.1) lru-cache: 10.4.3 socks-proxy-agent: 8.0.3(supports-color@8.1.1) transitivePeerDependencies: @@ -18517,7 +19802,7 @@ snapshots: dependencies: react: 18.3.1 - '@react-router/dev@7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5)': + '@react-router/dev@7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5)': dependencies: '@babel/core': 7.26.10(supports-color@8.1.1) '@babel/generator': 7.27.0 @@ -18528,7 +19813,7 @@ snapshots: '@babel/traverse': 7.27.0(supports-color@8.1.1) '@babel/types': 7.27.0 '@npmcli/package-json': 4.0.1 - '@react-router/node': 7.4.0(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.5.2) + '@react-router/node': 7.4.0(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.5.2) arg: 5.0.2 babel-dead-code-elimination: 1.0.9(supports-color@8.1.1) chokidar: 4.0.3 @@ -18542,7 +19827,7 @@ snapshots: picocolors: 1.1.1 prettier: 2.8.8 react-refresh: 0.14.2 - react-router: 7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router: 7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) semver: 7.7.1 set-cookie-parser: 2.7.1 valibot: 0.41.0(typescript@5.5.2) @@ -18566,17 +19851,17 @@ snapshots: - tsx - yaml - '@react-router/fs-routes@7.4.0(@react-router/dev@7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5))(typescript@5.5.2)': + '@react-router/fs-routes@7.4.0(@react-router/dev@7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5))(typescript@5.5.2)': dependencies: - '@react-router/dev': 7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5) + '@react-router/dev': 7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5) minimatch: 9.0.5 optionalDependencies: typescript: 5.5.2 - '@react-router/node@7.4.0(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.5.2)': + '@react-router/node@7.4.0(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.5.2)': dependencies: '@mjackson/node-fetch-server': 0.2.0 - react-router: 7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router: 7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) source-map-support: 0.5.21 stream-slice: 0.1.2 undici: 6.21.2 @@ -18777,6 +20062,8 @@ snapshots: - ajv - supports-color + '@repeaterjs/repeater@3.0.6': {} + '@resvg/resvg-wasm@2.4.0': {} '@rollup/plugin-alias@5.1.1(rollup@4.38.0)': @@ -20290,6 +21577,8 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/crypto-js@4.2.2': {} + '@types/d3-array@3.0.8': {} '@types/d3-axis@3.0.6': @@ -20454,6 +21743,10 @@ snapshots: '@types/geojson@7946.0.12': {} + '@types/graphql-validation-complexity@0.4.4': + dependencies: + graphql: 16.10.0 + '@types/hast@2.3.6': dependencies: '@types/unist': 2.0.8 @@ -20491,6 +21784,10 @@ snapshots: '@types/lodash@4.17.5': {} + '@types/lz-string@1.5.0': + dependencies: + lz-string: 1.5.0 + '@types/markdown-table@3.0.0': dependencies: markdown-table: 3.0.3 @@ -20501,7 +21798,7 @@ snapshots: '@types/mdast@4.0.1': dependencies: - '@types/unist': 3.0.3 + '@types/unist': 2.0.8 '@types/mdx-js__react@1.5.6': dependencies: @@ -20749,13 +22046,15 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vercel/flags@2.6.0(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@usercentrics/cmp-browser-sdk@4.42.0': dependencies: - jose: 5.2.1 - optionalDependencies: - next: 14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + '@types/crypto-js': 4.2.2 + '@types/lz-string': 1.5.0 + '@types/uuid': 8.3.4 + crypto-js: 4.2.0 + dynamic-import-polyfill: 0.1.1 + lz-string: 1.5.0 + uuid: 9.0.1 '@vercel/flags@2.6.0(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -21062,6 +22361,27 @@ snapshots: '@webgpu/types@0.1.49': {} + '@whatwg-node/disposablestack@0.0.6': + dependencies: + '@whatwg-node/promise-helpers': 1.3.1 + tslib: 2.8.1 + + '@whatwg-node/fetch@0.10.6': + dependencies: + '@whatwg-node/node-fetch': 0.7.18 + urlpattern-polyfill: 10.0.0 + + '@whatwg-node/node-fetch@0.7.18': + dependencies: + '@fastify/busboy': 3.1.1 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/promise-helpers': 1.3.1 + tslib: 2.8.1 + + '@whatwg-node/promise-helpers@1.3.1': + dependencies: + tslib: 2.8.1 + '@xobotyi/scrollbar-width@1.9.5': {} '@xtuc/ieee754@1.2.0': {} @@ -21389,6 +22709,8 @@ snapshots: is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 + asap@2.0.6: {} + assert-plus@1.0.0: {} assertion-error@2.0.1: {} @@ -21399,6 +22721,8 @@ snapshots: dependencies: tslib: 2.8.1 + astral-regex@2.0.0: {} + astring@1.8.6: {} async-sema@3.1.1: {} @@ -21411,6 +22735,8 @@ snapshots: attr-accept@2.2.5: {} + auto-bind@4.0.0: {} + autolinker@0.28.1: dependencies: gulp-header: 1.8.12 @@ -21517,6 +22843,12 @@ snapshots: bintrees@1.0.2: {} + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + bl@5.1.0: dependencies: buffer: 6.0.3 @@ -21565,12 +22897,21 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + buffer-crc32@1.0.0: {} buffer-from@1.1.2: {} buffer-writer@2.0.0: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -21709,6 +23050,19 @@ snapshots: chalk@5.4.1: {} + change-case-all@1.0.15: + dependencies: + change-case: 4.1.2 + is-lower-case: 2.0.2 + is-upper-case: 2.0.2 + lower-case: 2.0.2 + lower-case-first: 2.0.2 + sponge-case: 1.0.1 + swap-case: 2.0.2 + title-case: 3.0.3 + upper-case: 2.0.2 + upper-case-first: 2.0.2 + change-case@4.1.2: dependencies: camel-case: 4.1.2 @@ -21740,6 +23094,8 @@ snapshots: character-reference-invalid@2.0.1: {} + chardet@0.7.0: {} + check-error@2.1.1: {} cheerio-select@2.1.0: @@ -21819,12 +23175,23 @@ snapshots: cli-boxes@3.0.0: {} + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + cli-cursor@4.0.0: dependencies: restore-cursor: 4.0.0 cli-spinners@2.9.2: {} + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + + cli-width@3.0.0: {} + cli-width@4.1.0: {} client-only@0.0.1: {} @@ -21889,13 +23256,21 @@ snapshots: estree-walker: 3.0.3 periscopic: 3.1.0 - codemirror-graphql@2.0.10(@codemirror/language@6.11.0)(codemirror@5.65.15)(graphql@16.8.1): + codemirror-graphql@2.0.10(@codemirror/language@6.11.0)(codemirror@5.65.15)(graphql@16.10.0): dependencies: '@codemirror/language': 6.11.0 '@types/codemirror': 0.0.90 codemirror: 5.65.15 - graphql: 16.8.1 - graphql-language-service: 5.2.0(graphql@16.8.1) + graphql: 16.10.0 + graphql-language-service: 5.2.0(graphql@16.10.0) + + codemirror-graphql@2.2.0(@codemirror/language@6.11.0)(codemirror@5.65.15)(graphql@16.10.0): + dependencies: + '@codemirror/language': 6.11.0 + '@types/codemirror': 0.0.90 + codemirror: 5.65.15 + graphql: 16.10.0 + graphql-language-service: 5.3.0(graphql@16.10.0) codemirror@5.65.15: {} @@ -22092,6 +23467,15 @@ snapshots: optionalDependencies: typescript: 5.5.2 + cosmiconfig@9.0.0(typescript@5.5.2): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.5.2 + crc-32@1.2.2: {} crc32-stream@6.0.0: @@ -22112,6 +23496,16 @@ snapshots: cronstrue@2.50.0: {} + cross-fetch@3.2.0(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + cross-inspect@1.0.1: + dependencies: + tslib: 2.8.1 + cross-spawn@6.0.6: dependencies: nice-try: 1.0.5 @@ -22297,6 +23691,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + dataloader@2.2.3: {} + date-fns@2.30.0: dependencies: '@babel/runtime': 7.26.10 @@ -22409,6 +23805,8 @@ snapshots: depd@2.0.0: {} + dependency-graph@0.11.0: {} + dequal@2.0.3: {} derive-valtio@0.1.0(valtio@1.12.0(@types/react@18.3.3)(react@18.3.1)): @@ -22419,6 +23817,8 @@ snapshots: destroy@1.2.0: {} + detect-indent@6.1.0: {} + detect-libc@1.0.3: {} detect-libc@2.0.3: {} @@ -22523,6 +23923,8 @@ snapshots: drange@1.1.1: {} + dset@3.1.4: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -22531,6 +23933,8 @@ snapshots: duplexer@0.1.2: {} + dynamic-import-polyfill@0.1.1: {} + eastasianwidth@0.2.0: {} ee-first@1.1.1: {} @@ -22893,7 +24297,7 @@ snapshots: debug: 4.4.0(supports-color@8.1.1) enhanced-resolve: 5.17.1 eslint: 8.57.0(supports-color@8.1.1) - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) fast-glob: 3.3.2 get-tsconfig: 4.7.2 @@ -22905,7 +24309,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: @@ -22932,7 +24336,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0(supports-color@8.1.1) eslint-import-resolver-node: 0.3.9(supports-color@8.1.1) - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -23206,6 +24610,12 @@ snapshots: extend@3.0.2: {} + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + extsprintf@1.4.1: {} fast-content-type-parse@1.1.0: {} @@ -23317,6 +24727,24 @@ snapshots: dependencies: format: 0.2.2 + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fbjs-css-vars@1.0.2: {} + + fbjs@3.0.5(encoding@0.1.13): + dependencies: + cross-fetch: 3.2.0(encoding@0.1.13) + fbjs-css-vars: 1.0.2 + loose-envify: 1.4.0 + object-assign: 4.1.1 + promise: 7.3.1 + setimmediate: 1.0.5 + ua-parser-js: 1.0.40 + transitivePeerDependencies: + - encoding + fdir@6.4.3(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -23330,6 +24758,10 @@ snapshots: fflate@0.8.2: {} + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + file-entry-cache@6.0.1: dependencies: flat-cache: 3.1.0 @@ -23586,6 +25018,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-value@3.0.1: + dependencies: + isobject: 3.0.1 + giget@2.0.0: dependencies: citty: 0.1.6 @@ -23680,7 +25116,7 @@ snapshots: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.3.2 + fast-glob: 3.3.3 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 @@ -23714,15 +25150,87 @@ snapshots: graphemer@1.4.0: {} - graphql-language-service@5.2.0(graphql@16.8.1): + graphiql@3.8.3(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - graphql: 16.8.1 + '@graphiql/react': 0.28.2(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + graphql: 16.10.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@codemirror/language' + - '@types/node' + - '@types/react' + - '@types/react-dom' + - graphql-ws + + graphql-config@5.1.4(@types/node@20.12.11)(graphql@16.10.0)(typescript@5.5.2): + dependencies: + '@graphql-tools/graphql-file-loader': 8.0.19(graphql@16.10.0) + '@graphql-tools/json-file-loader': 8.0.18(graphql@16.10.0) + '@graphql-tools/load': 8.1.0(graphql@16.10.0) + '@graphql-tools/merge': 9.0.24(graphql@16.10.0) + '@graphql-tools/url-loader': 8.0.31(@types/node@20.12.11)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + cosmiconfig: 9.0.0(typescript@5.5.2) + graphql: 16.10.0 + jiti: 2.4.2 + minimatch: 10.0.1 + string-env-interpolation: 1.0.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - typescript + - uWebSockets.js + - utf-8-validate + + graphql-language-service@5.2.0(graphql@16.10.0): + dependencies: + graphql: 16.10.0 nullthrows: 1.1.1 vscode-languageserver-types: 3.17.5 - graphql-ws@5.14.1(graphql@16.8.1): + graphql-language-service@5.3.0(graphql@16.10.0): dependencies: - graphql: 16.8.1 + debounce-promise: 3.1.2 + graphql: 16.10.0 + nullthrows: 1.1.1 + vscode-languageserver-types: 3.17.5 + + graphql-request@6.1.0(encoding@0.1.13)(graphql@16.10.0): + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) + cross-fetch: 3.2.0(encoding@0.1.13) + graphql: 16.10.0 + transitivePeerDependencies: + - encoding + + graphql-sock@1.0.1(graphql@16.10.0): + dependencies: + graphql: 16.10.0 + + graphql-tag@2.12.6(graphql@16.10.0): + dependencies: + graphql: 16.10.0 + tslib: 2.8.1 + + graphql-validation-complexity@0.4.2(graphql@16.10.0): + dependencies: + graphql: 16.10.0 + warning: 4.0.3 + + graphql-ws@5.14.1(graphql@16.10.0): + dependencies: + graphql: 16.10.0 + + graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1): + dependencies: + graphql: 16.10.0 + optionalDependencies: + ws: 8.18.1 + + graphql@16.10.0: {} graphql@16.8.1: {} @@ -24133,7 +25641,7 @@ snapshots: http-proxy-agent@7.0.2(supports-color@8.1.1): dependencies: - agent-base: 7.1.1(supports-color@8.1.1) + agent-base: 7.1.3 debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -24206,6 +25714,10 @@ snapshots: hyphenate-style-name@1.0.4: {} + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -24223,6 +25735,8 @@ snapshots: immutability-helper@3.1.1: {} + immutable@3.7.6: {} + immutable@4.3.4: {} import-fresh@3.3.0: @@ -24230,6 +25744,8 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-from@4.0.0: {} + import-in-the-middle@1.13.1: dependencies: acorn: 8.14.1 @@ -24272,6 +25788,24 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + inquirer@8.2.6: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + internal-slot@1.0.7: dependencies: es-errors: 1.3.0 @@ -24282,6 +25816,10 @@ snapshots: intersection-observer@0.10.0: {} + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ioredis@5.6.0(supports-color@8.1.1): dependencies: '@ioredis/commands': 1.2.0 @@ -24307,6 +25845,11 @@ snapshots: iron-webcrypto@1.2.1: {} + is-absolute@1.0.0: + dependencies: + is-relative: 1.0.0 + is-windows: 1.0.2 + is-alphabetical@1.0.4: {} is-alphabetical@2.0.1: {} @@ -24407,10 +25950,16 @@ snapshots: dependencies: is-docker: 3.0.0 + is-interactive@1.0.0: {} + is-interactive@2.0.0: {} is-lambda@1.0.1: {} + is-lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + is-map@2.0.2: {} is-module@1.0.0: {} @@ -24463,6 +26012,10 @@ snapshots: is-regexp@3.1.0: {} + is-relative@1.0.0: + dependencies: + is-unc-path: 1.0.0 + is-set@2.0.2: {} is-shared-array-buffer@1.0.3: @@ -24485,8 +26038,18 @@ snapshots: dependencies: which-typed-array: 1.1.15 + is-unc-path@1.0.0: + dependencies: + unc-path-regex: 0.1.2 + + is-unicode-supported@0.1.0: {} + is-unicode-supported@1.3.0: {} + is-upper-case@2.0.2: + dependencies: + tslib: 2.8.1 + is-weakmap@2.0.1: {} is-weakref@1.0.2: @@ -24500,6 +26063,8 @@ snapshots: is-what@4.1.15: {} + is-windows@1.0.2: {} + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 @@ -24530,6 +26095,10 @@ snapshots: isobject@3.0.1: {} + isomorphic-ws@5.0.0(ws@8.18.1): + dependencies: + ws: 8.18.1 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -24726,6 +26295,11 @@ snapshots: json-stringify-safe@5.0.1: {} + json-to-pretty-yaml@1.2.2: + dependencies: + remedial: 1.0.8 + remove-trailing-spaces: 1.0.9 + json5@1.0.2: dependencies: minimist: 1.2.8 @@ -24850,6 +26424,10 @@ snapshots: dependencies: uc.micro: 1.0.6 + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + list-item@1.1.1: dependencies: expand-range: 1.8.2 @@ -24878,6 +26456,17 @@ snapshots: untun: 0.1.3 uqr: 0.1.2 + listr2@4.0.5: + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.4.1 + rxjs: 7.8.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + load-json-file@4.0.0: dependencies: graceful-fs: 4.2.11 @@ -24946,6 +26535,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash.sortby@4.7.0: {} + lodash.template@4.5.0: dependencies: lodash._reinterpolate: 3.0.0 @@ -24964,11 +26555,23 @@ snapshots: lodash@4.17.21: {} + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + log-symbols@5.1.0: dependencies: chalk: 5.4.1 is-unicode-supported: 1.3.0 + log-update@4.0.0: + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + long@5.2.3: {} longest-streak@3.1.0: {} @@ -24981,6 +26584,10 @@ snapshots: loupe@3.1.3: {} + lower-case-first@2.0.2: + dependencies: + tslib: 2.8.1 + lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -25078,6 +26685,8 @@ snapshots: - bluebird - supports-color + map-cache@0.2.2: {} + mark.js@8.11.1: {} markdown-extensions@1.1.1: {} @@ -25092,6 +26701,15 @@ snapshots: mdurl: 1.0.1 uc.micro: 1.0.6 + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-link@0.1.1: {} markdown-table@3.0.3: {} @@ -25475,6 +27093,8 @@ snapshots: mdurl@1.0.1: {} + mdurl@2.0.0: {} + mdx-bundler@10.0.2(esbuild@0.25.2)(supports-color@8.1.1): dependencies: '@babel/runtime': 7.26.10 @@ -26266,7 +27886,7 @@ snapshots: '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 '@types/statuses': 2.0.5 - graphql: 16.8.1 + graphql: 16.10.0 headers-polyfill: 4.0.3 is-node-process: 1.2.0 outvariant: 1.4.3 @@ -26292,7 +27912,7 @@ snapshots: '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 '@types/statuses': 2.0.5 - graphql: 16.8.1 + graphql: 16.10.0 headers-polyfill: 4.0.3 is-node-process: 1.2.0 outvariant: 1.4.3 @@ -26306,6 +27926,8 @@ snapshots: transitivePeerDependencies: - '@types/node' + mute-stream@0.0.8: {} + mute-stream@1.0.0: {} mute-stream@2.0.0: {} @@ -26390,11 +28012,6 @@ snapshots: dependencies: js-yaml-loader: 1.2.2 - next-router-mock@0.9.13(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react@18.3.1): - dependencies: - next: 14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0) - react: 18.3.1 - next-router-mock@0.9.13(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react@18.3.1): dependencies: next: 15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0) @@ -26641,6 +28258,8 @@ snapshots: - bluebird - supports-color + node-int64@0.4.0: {} + node-mock-http@1.0.0: {} node-pty@1.0.0: @@ -26686,6 +28305,10 @@ snapshots: semver: 7.7.1 validate-npm-package-license: 3.0.4 + normalize-path@2.1.1: + dependencies: + remove-trailing-separator: 1.1.0 + normalize-path@3.0.0: {} normalize-range@0.1.2: {} @@ -26748,18 +28371,18 @@ snapshots: number-flow@0.3.7: {} - nuqs@1.19.1(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)): + nuqs@1.19.1(next@14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)): dependencies: mitt: 3.0.1 next: 14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0) - nuqs@2.4.1(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + nuqs@2.4.1(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: mitt: 3.0.1 react: 18.3.1 optionalDependencies: next: 15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0) - react-router: 7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router: 7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nwsapi@2.2.20: optional: true @@ -26972,6 +28595,18 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + ora@6.3.1: dependencies: chalk: 5.4.1 @@ -26984,6 +28619,8 @@ snapshots: strip-ansi: 7.1.0 wcwidth: 1.0.1 + os-tmpdir@1.0.2: {} + outdent@0.8.0: {} outvariant@1.4.0: {} @@ -27044,6 +28681,12 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse-filepath@1.0.2: + dependencies: + is-absolute: 1.0.0 + map-cache: 0.2.2 + path-root: 0.1.1 + parse-json@4.0.0: dependencies: error-ex: 1.3.2 @@ -27104,6 +28747,12 @@ snapshots: path-parse@1.0.7: {} + path-root-regex@0.1.2: {} + + path-root@0.1.1: + dependencies: + path-root-regex: 0.1.2 + path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 @@ -27495,6 +29144,10 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 + promise@7.3.1: + dependencies: + asap: 2.0.6 + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -27548,6 +29201,8 @@ snapshots: psl@1.9.0: {} + punycode.js@2.3.1: {} + punycode@2.3.0: {} punycode@2.3.1: @@ -27621,6 +29276,10 @@ snapshots: transitivePeerDependencies: - react-native + react-compiler-runtime@19.0.0-beta-37ed2a7-20241206(react@18.3.1): + dependencies: + react: 18.3.1 + react-contexify@5.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: clsx: 1.2.1 @@ -27876,9 +29535,8 @@ snapshots: transitivePeerDependencies: - react-dom - react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@types/cookie': 0.6.0 cookie: 1.0.2 react: 18.3.1 set-cookie-parser: 2.7.1 @@ -28214,6 +29872,14 @@ snapshots: hast-util-to-html: 9.0.0 unified: 11.0.5 + relay-runtime@12.0.0(encoding@0.1.13): + dependencies: + '@babel/runtime': 7.26.10 + fbjs: 3.0.5(encoding@0.1.13) + invariant: 2.2.4 + transitivePeerDependencies: + - encoding + remark-code-import@1.2.0: dependencies: strip-indent: 4.0.0 @@ -28362,8 +30028,14 @@ snapshots: argparse: 1.0.10 autolinker: 0.28.1 + remedial@1.0.8: {} + remove-accents@0.4.2: {} + remove-trailing-separator@1.1.0: {} + + remove-trailing-spaces@1.0.9: {} + rename-keys@1.2.0: {} repeat-element@1.1.4: {} @@ -28412,6 +30084,11 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + restore-cursor@4.0.0: dependencies: onetime: 5.1.2 @@ -28491,6 +30168,8 @@ snapshots: run-applescript@7.0.0: {} + run-async@2.4.1: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -28526,8 +30205,7 @@ snapshots: safe-stable-stringify@2.5.0: {} - safer-buffer@2.1.2: - optional: true + safer-buffer@2.1.2: {} sass@1.72.0: dependencies: @@ -28576,6 +30254,8 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.1 + scuid@1.1.0: {} + scule@1.3.0: {} section-matter@1.0.0: @@ -28667,6 +30347,8 @@ snapshots: is-plain-object: 2.0.4 is-primitive: 3.0.1 + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} shadcn@2.4.0-canary.16(@types/node@22.13.14)(supports-color@8.1.1)(typescript@5.5.2): @@ -28799,6 +30481,8 @@ snapshots: signal-exit@4.1.0: {} + signedsource@1.0.0: {} + simple-git@3.24.0(supports-color@8.1.1): dependencies: '@kwsites/file-exists': 1.1.1(supports-color@8.1.1) @@ -28843,6 +30527,18 @@ snapshots: slash@5.1.0: {} + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + slugify@1.4.7: {} slugify@1.6.6: {} @@ -28868,7 +30564,7 @@ snapshots: socks-proxy-agent@8.0.3(supports-color@8.1.1): dependencies: - agent-base: 7.1.1(supports-color@8.1.1) + agent-base: 7.1.3 debug: 4.4.0(supports-color@8.1.1) socks: 2.8.3 transitivePeerDependencies: @@ -28931,6 +30627,10 @@ snapshots: split2@4.2.0: {} + sponge-case@1.0.1: + dependencies: + tslib: 2.8.1 + sprintf-js@1.0.3: {} sprintf-js@1.1.3: {} @@ -29030,6 +30730,8 @@ snapshots: strict-event-emitter@0.5.1: {} + string-env-interpolation@1.0.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -29281,6 +30983,10 @@ snapshots: transitivePeerDependencies: - encoding + swap-case@2.0.2: + dependencies: + tslib: 2.8.1 + swiper@11.0.7: {} swr@2.2.5(react@18.3.1): @@ -29298,6 +31004,12 @@ snapshots: symbol-tree@3.2.4: optional: true + sync-fetch@0.6.0-2: + dependencies: + node-fetch: 3.3.2 + timeout-signal: 2.0.0 + whatwg-mimetype: 4.0.0 + system-architecture@0.1.0: {} tailwind-merge@1.14.0: {} @@ -29463,6 +31175,10 @@ snapshots: readable-stream: 2.3.8 xtend: 4.0.2 + through@2.3.8: {} + + timeout-signal@2.0.0: {} + tiny-case@1.0.3: {} tiny-inflate@1.0.3: {} @@ -29493,6 +31209,14 @@ snapshots: tinyspy@3.0.2: {} + title-case@3.0.3: + dependencies: + tslib: 2.8.1 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + to-fast-properties@2.0.0: {} to-gatsby-remark-plugin@0.1.0: @@ -29562,6 +31286,8 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-log@2.2.7: {} + ts-morph@18.0.0: dependencies: '@ts-morph/common': 0.19.0 @@ -29751,8 +31477,12 @@ snapshots: typescript@5.5.2: {} + ua-parser-js@1.0.40: {} + uc.micro@1.0.6: {} + uc.micro@2.1.0: {} + ufo@1.5.4: {} uglify-js@3.17.4: @@ -29769,6 +31499,8 @@ snapshots: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 + unc-path-regex@0.1.2: {} + uncrypto@0.1.3: {} unctx@2.4.1: @@ -29957,6 +31689,10 @@ snapshots: universalify@2.0.0: {} + unixify@1.0.0: + dependencies: + normalize-path: 2.1.1 + unplugin-utils@0.2.4: dependencies: pathe: 2.0.3 @@ -30051,6 +31787,8 @@ snapshots: url-template@2.0.8: {} + urlpattern-polyfill@10.0.0: {} + use-callback-ref@1.3.3(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 @@ -30635,6 +32373,8 @@ snapshots: whatwg-mimetype@3.0.0: optional: true + whatwg-mimetype@4.0.0: {} + whatwg-url@11.0.0: dependencies: tr46: 3.0.0 @@ -30792,7 +32532,7 @@ snapshots: yargs@17.7.2: dependencies: cliui: 8.0.1 - escalade: 3.1.2 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d0cd97f3edc..137fe7e0cf8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -13,3 +13,4 @@ catalog: 'react-dom': '^18.3.0' '@types/react': '^18.3.0' '@types/react-dom': '^18.3.0' + 'valtio': '^1.12.0' diff --git a/supabase/migrations/20250423133137_improve_vector_search.sql b/supabase/migrations/20250423133137_improve_vector_search.sql new file mode 100644 index 00000000000..8563164e5bd --- /dev/null +++ b/supabase/migrations/20250423133137_improve_vector_search.sql @@ -0,0 +1,103 @@ +create index if not exists +idx_page_section_embedding +on public.page_section +using hnsw (embedding vector_ip_ops); + +create or replace function match_embedding( + embedding vector(1536), + match_threshold float default 0.78, + max_results int default 30 +) +returns setof page_section +language plpgsql +set search_path = '' +as $$ +#variable_conflict use_variable +begin + return query + select * + from public.page_section + where (page_section.embedding operator(public.<#>) embedding) <= -match_threshold + order by page_section.embedding operator(public.<#>) embedding + limit max_results; +end; +$$; + +create or replace function get_full_content_url( + type text, + path text, + slug text +) +returns text +language sql +set search_path = '' +as $$ + select case + when type = 'github-discussions' + then path + when type = 'partner-integration' + then concat('https://supabase.com', path) + else + concat( + 'https://supabase.com/docs', + path, + case + when slug is null + then '' + else + concat('#', slug) + end + ) + end; +$$; + +create or replace function search_content( + embedding vector(1536), + include_full_content boolean default false, + match_threshold float default 0.78, + max_result int default 30 +) +returns table ( + id bigint, + page_title text, + type text, + href text, + content text, + subsections json[] +) +language sql +set search_path = '' +as $$ + with matched_section as ( + select + *, + row_number() over () as ranking + from public.match_embedding( + embedding, + match_threshold, + max_result + ) + ) + select + page.id, + meta ->> 'title' as page_title, + type, + public.get_full_content_url(type, path, null) as href, + case + when include_full_content + then page.content + else + null + end as content, + array_agg( + json_build_object( + 'title', heading, + 'href', public.get_full_content_url(type, path, slug), + 'content', matched_section.content + ) + ) + from matched_section + join public.page on matched_section.page_id = page.id + group by page.id + order by min(ranking); +$$; diff --git a/turbo.json b/turbo.json index d2d085b1a69..316d1964807 100644 --- a/turbo.json +++ b/turbo.json @@ -49,6 +49,7 @@ "NEXT_PUBLIC_AUTH_NAVIGATOR_LOCK_KEY", "NEXT_PUBLIC_AUTH_DETECT_SESSION_IN_URL", "NEXT_PUBLIC_VERCEL_ENV", + "NEXT_PUBLIC_USERCENTRICS_RULESET_ID", // These envs are technically passthrough env vars because they're only used on the server side of Nextjs "PLATFORM_PG_META_URL", "STUDIO_PG_META_URL", @@ -105,6 +106,7 @@ "NEXT_PUBLIC_BASE_PATH", "NEXT_PUBLIC_EXAMPLES_SUPABASE_ANON_KEY", "NEXT_PUBLIC_EXAMPLES_SUPABASE_URL", + "NEXT_PUBLIC_USERCENTRICS_RULESET_ID", // These envs are technically passthrough env vars because they're only used on the server side of Nextjs "LIVE_SUPABASE_COM_SERVICE_ROLE_KEY", "GITHUB_CHANGELOG_APP_ID", @@ -144,7 +146,8 @@ "NEXT_PUBLIC_AUTH_NAVIGATOR_LOCK_KEY", "NEXT_PUBLIC_AUTH_DETECT_SESSION_IN_URL", "NEXT_PUBLIC_GOTRUE_URL", - "NEXT_PUBLIC_SUPABASE_ANON_KEY" + "NEXT_PUBLIC_SUPABASE_ANON_KEY", + "NEXT_PUBLIC_USERCENTRICS_RULESET_ID" ], "outputs": [".next/**", "!.next/cache/**", ".contentlayer/**"] },