More scaffolding

This commit is contained in:
Joshen Lim committed 2024-05-02 12:26:26 +08:00
1 parent a53240fb5b
commit 7253726dde
12 files changed
+346 -123

No files matched your search

@@ -62,6 +62,7 @@ import {
createSqlSnippetSkeleton,
} from './SQLEditor.utils'
import UtilityPanel from './UtilityPanel/UtilityPanel'
import { Settings, X } from 'lucide-react'
// Load the monaco editor client-side only (does not behave well server-side)
const MonacoEditor = dynamic(() => import('./MonacoEditor'), { ssr: false })
@@ -248,6 +249,7 @@ const SQLEditor = () => {
const snippet = id ? snap.snippets[id] : null
const isLoading = urlId === 'new' ? false : !(id && ref && snap.loaded[ref])
console.log({ isLoading })
/**
* Sets the snippet title using AI.
@@ -802,15 +804,15 @@ const SQLEditor = () => {
setIsSchemaSuggestionDismissed(true)
appSnap.setShowAiSettingsModal(true)
}}
className="text-brand-600 hover:text-brand-600 transition"
className="text-brand-500 hover:text-brand-600 transition"
>
<IconSettings className="cursor-pointer" />
<Settings size={20} className="cursor-pointer" />
</button>
<button
className="transition text-brand-500 hover:text-brand-600"
onClick={() => setIsAiOpen(false)}
>
<IconX size={21} />
<X size={21} />
</button>
</>
)}
@@ -9,7 +9,6 @@ import { untitledSnippetTitle } from 'components/interfaces/SQLEditor/SQLEditor.
import { createSqlSnippetSkeleton } from 'components/interfaces/SQLEditor/SQLEditor.utils'
import { useContentDeleteMutation } from 'data/content/content-delete-mutation'
import { useFolderCreateMutation } from 'data/content/folder-create-mutation'
import { useSQLSnippetFoldersQuery } from 'data/content/folders-query'
import { SqlSnippet, useSqlSnippetsQuery } from 'data/content/sql-snippets-query'
import { useCheckPermissions, useFlag, useSelectedProject } from 'hooks'
import { uuidv4 } from 'lib/helpers'
@@ -19,7 +18,7 @@ import { Button } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { InnerSideMenuItem } from 'ui-patterns/InnerSideMenu'
import { SQLEditorNavV1 } from './SQLEditorNavV1'
import SQLEditorNavV2 from './SQLEditorNavV2'
import { SQLEditorNav as SQLEditorNavV2 } from './SQLEditorNavV2/SQLEditorNav'
const SideBarContent = () => {
const router = useRouter()
@@ -42,18 +41,6 @@ const SideBarContent = () => {
},
})
useSQLSnippetFoldersQuery(
{ projectRef: ref },
{
enabled: enableFolders,
refetchOnWindowFocus: false,
staleTime: 5 * 60 * 1000, // 5 minutes.
onSuccess(data) {
if (ref) console.log(data)
},
}
)
const { mutate: createFolder, isLoading: isCreating } = useFolderCreateMutation({
onSuccess: () => {
toast.success('Done')
@@ -1,82 +0,0 @@
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
TreeView,
TreeViewItem,
flattenTree,
} from 'ui'
export const SQLEditorNavV2 = () => {
const MOCK_DATA = {
name: '',
children: [
{
name: 'Current batch',
},
{
name: 'Older queries',
},
{
name: 'query all users',
},
{
name: 'users in last day',
},
{
name: 'new users over time',
},
],
}
return (
<TreeView
data={flattenTree(MOCK_DATA)}
className=""
aria-label="directory tree"
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => (
<ContextMenu modal={false}>
<ContextMenuTrigger asChild>
<TreeViewItem
level={level}
xPadding={0}
name={element.name}
isExpanded={isExpanded}
isBranch={isBranch}
isSelected={isSelected}
isEditing={element.metadata?.isEditing === true}
// onEditSubmit={(value) => {
// let updatedTreeData = { ...treeData }
// const findNode = (node: any) => {
// if (node.id === element.id) {
// node.name = value
// node.metadata = { isEditing: false }
// }
// if (node.children) {
// node.children.forEach(findNode)
// }
// }
// updatedTreeData.children.forEach(findNode)
// setDataTreeState(updatedTreeData)
// }}
{...getNodeProps()}
/>
</ContextMenuTrigger>
<ContextMenuContent onCloseAutoFocus={(e) => e.stopPropagation()}>
<ContextMenuItem onSelect={(e) => {}} onFocusCapture={(e) => e.stopPropagation()}>
Rename
</ContextMenuItem>
<ContextMenuItem disabled>Open in new tab</ContextMenuItem>
<ContextMenuItem disabled>Share with team</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem disabled>Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)}
/>
)
}
export default SQLEditorNavV2
@@ -0,0 +1,120 @@
import { useParams } from 'common'
import { useSQLSnippetFoldersQuery } from 'data/content/folders-query'
import {
CollapsibleContent_Shadcn_,
CollapsibleTrigger_Shadcn_,
Collapsible_Shadcn_,
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
Separator,
TreeView,
TreeViewItem,
flattenTree,
} from 'ui'
import { ROOT_NODE, TreeViewItemProps, formatFolderResponseForTreeView } from './SQLEditorNav.utils'
import { ChevronDown, ChevronRight, ChevronUp } from 'lucide-react'
import { useState } from 'react'
import { useRouter } from 'next/router'
// Requirements
// - Asynchronous loading
// - Directory tree
// - Multi select
// - Context menu
export const SQLEditorNav = () => {
const router = useRouter()
const { ref: projectRef } = useParams()
const [treeState, setTreeState] = useState<TreeViewItemProps[]>([ROOT_NODE])
const [showPrivateSnippets, setShowPrivateSnippets] = useState(true)
useSQLSnippetFoldersQuery(
{ projectRef },
{
refetchOnWindowFocus: false,
staleTime: 5 * 60 * 1000, // 5 minutes
onSuccess: (data) => {
const entities = formatFolderResponseForTreeView(data)
setTreeState(entities)
},
}
)
return (
<>
<Separator />
<Collapsible_Shadcn_ open={showPrivateSnippets} onOpenChange={setShowPrivateSnippets}>
<CollapsibleTrigger_Shadcn_ className="flex items-center gap-x-2 px-4 [&[data-state=open]>svg]:!rotate-90">
<ChevronRight
className="text-foreground-light transition-transform duration-200"
size={16}
/>
<span className="text-foreground-light font-mono text-sm">PRIVATE</span>
</CollapsibleTrigger_Shadcn_>
<CollapsibleContent_Shadcn_ className="pt-2">
<TreeView
data={treeState}
className=""
aria-label="directory tree"
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => (
<ContextMenu modal={false}>
<ContextMenuTrigger asChild>
<TreeViewItem
level={level}
xPadding={16}
name={element.name}
isExpanded={isExpanded}
isBranch={isBranch}
isSelected={isSelected}
isEditing={element.metadata?.isEditing === true}
// onEditSubmit={(value) => {
// let updatedTreeData = { ...treeData }
// const findNode = (node: any) => {
// if (node.id === element.id) {
// node.name = value
// node.metadata = { isEditing: false }
// }
// if (node.children) {
// node.children.forEach(findNode)
// }
// }
// updatedTreeData.children.forEach(findNode)
// setDataTreeState(updatedTreeData)
// }}
{...getNodeProps()}
onClick={() => {
if (!isBranch) router.push(`/project/${projectRef}/sql/${element.id}`)
}}
/>
</ContextMenuTrigger>
<ContextMenuContent onCloseAutoFocus={(e) => e.stopPropagation()}>
{isBranch ? (
<>
<ContextMenuItem disabled>New snippet</ContextMenuItem>
</>
) : (
<>
<ContextMenuItem disabled>Open in new tab</ContextMenuItem>
<ContextMenuItem disabled>Share with team</ContextMenuItem>
</>
)}
<ContextMenuSeparator />
<ContextMenuItem onSelect={(e) => {}} onFocusCapture={(e) => e.stopPropagation()}>
Rename
</ContextMenuItem>
<ContextMenuItem disabled>Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)}
/>
</CollapsibleContent_Shadcn_>
</Collapsible_Shadcn_>
<Separator />
</>
)
}
@@ -0,0 +1,47 @@
import { SnippetFolderResponse } from 'data/content/folders-query'
export interface TreeViewItemProps {
id: string | number
name: string
parent: number | null
children: any[]
metadata?: any
}
export const ROOT_NODE = { id: 0, name: '', parent: null, children: [] }
export const splitProjectPrivateSnippets = () => {
// TODO
return { project: [], private: [] }
}
export const formatFolderResponseForTreeView = (
response?: SnippetFolderResponse
): TreeViewItemProps[] => {
if (response === undefined) return [ROOT_NODE]
const { folders, contents } = response
const formattedFolders =
folders?.map((folder) => {
const { id, name, ...metadata } = folder
return { id, name, parent: 0, isBranch: true, children: [], metadata }
}) || []
const formattedContents =
contents?.map((content) => {
const { id, name, ...metadata } = content
return { id, name, parent: 0, children: [], metadata }
}) || []
const root = {
id: 0,
name: '',
parent: null,
children: [
...(folders || [])?.map((folder) => folder.id),
...(contents || [])?.map((content) => content.id),
],
}
return [root, ...formattedFolders, ...formattedContents]
}
@@ -0,0 +1,3 @@
export const SQLEditorNavTreeItem = () => {
return <div></div>
}
@@ -0,0 +1,37 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get, handleError } from 'data/fetchers'
import { ResponseError } from 'types'
import { contentKeys } from './keys'
export async function getContentById(
{ projectRef, id }: { projectRef?: string; id?: string },
signal?: AbortSignal
) {
if (typeof projectRef === 'undefined') throw new Error('projectRef is required')
if (typeof id === 'undefined') throw new Error('Content ID is required')
const { data, error } = await get('/platform/projects/{ref}/content/item/{id}', {
params: { path: { ref: projectRef, id } },
signal,
})
if (error) throw handleError(error)
return data
}
export type ContentIdData = Awaited<ReturnType<typeof getContentById>>
export type ContentIdError = ResponseError
export const useContentIdQuery = <TData = ContentIdData>(
{ projectRef, id }: { projectRef?: string; id?: string },
{ enabled = true, ...options }: UseQueryOptions<ContentIdData, ContentIdError, TData> = {}
) =>
useQuery<ContentIdData, ContentIdError, TData>(
contentKeys.resource(projectRef, id),
({ signal }) => getContentById({ projectRef, id }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined',
...options,
}
)
@@ -7,7 +7,7 @@ import type { ResponseError } from 'types'
import type { Content } from './content-query'
import { contentKeys } from './keys'
export type InsertContentPayload = Omit<components['schemas']['CreateContentParams'], 'content'> & {
export type InsertContentPayload = Omit<components['schemas']['CreateContentBody'], 'content'> & {
content: Content['content']
}
+20 -9
View File
@@ -10,32 +10,43 @@ export type SnippetFolder = components['schemas']['UserContentFolder']
export type Snippet = components['schemas']['UserContentObjectMeta']
export async function getSQLSnippetFolders(
{ projectRef }: { projectRef: string | undefined },
{ projectRef, folderId }: { projectRef?: string; folderId?: string },
signal?: AbortSignal
) {
if (typeof projectRef === 'undefined') throw new Error('projectRef is required')
const { data, error } = await get('/platform/projects/{ref}/content/folders', {
params: { path: { ref: projectRef } },
signal,
})
if (folderId) {
const { data, error } = await get('/platform/projects/{ref}/content/folders/{id}', {
params: { path: { ref: projectRef, id: folderId } },
signal,
})
if (error) throw handleError(error)
return data.data
if (error) throw handleError(error)
return data.data
} else {
const { data, error } = await get('/platform/projects/{ref}/content/folders', {
params: { path: { ref: projectRef }, query: { type: 'sql' } },
signal,
})
if (error) throw handleError(error)
return data.data
}
}
export type SQLSnippetFoldersData = Awaited<ReturnType<typeof getSQLSnippetFolders>>
export type SQLSnippetFoldersError = ResponseError
export const useSQLSnippetFoldersQuery = <TData = SQLSnippetFoldersData>(
{ projectRef }: { projectRef: string | undefined },
{ projectRef, folderId }: { projectRef?: string; folderId?: string },
{
enabled = true,
...options
}: UseQueryOptions<SQLSnippetFoldersData, SQLSnippetFoldersError, TData> = {}
) =>
useQuery<SQLSnippetFoldersData, SQLSnippetFoldersError, TData>(
// [Joshen] TODO: Need to think about how to store the query key
contentKeys.folders(projectRef),
({ signal }) => getSQLSnippetFolders({ projectRef }, signal),
({ signal }) => getSQLSnippetFolders({ projectRef, folderId }, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
+2
View File
@@ -2,4 +2,6 @@ export const contentKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'content'] as const,
folders: (projectRef: string | undefined) =>
['projects', projectRef, 'content', 'folders'] as const,
resource: (projectRef: string | undefined, id: string | undefined) =>
['projects', projectRef, 'content', id] as const,
}
+8 -1
View File
@@ -15,10 +15,11 @@ import { SQLEditorLayout } from 'components/layouts'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import getPgsqlCompletionProvider from 'components/ui/CodeEditor/Providers/PgSQLCompletionProvider'
import getPgsqlSignatureHelpProvider from 'components/ui/CodeEditor/Providers/PgSQLSignatureHelpProvider'
import { useLocalStorageQuery } from 'hooks'
import { useFlag, useLocalStorageQuery } from 'hooks'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { useSnippets, useSqlEditorStateSnapshot } from 'state/sql-editor'
import { useContentIdQuery } from 'data/content/content-id-query'
const SqlEditor: NextPageWithLayout = () => {
const router = useRouter()
@@ -28,10 +29,16 @@ const SqlEditor: NextPageWithLayout = () => {
const { project } = useProjectContext()
const snap = useSqlEditorStateSnapshot()
const appSnap = useAppStateSnapshot()
const enableFolders = useFlag('sqlFolderOrganization')
const snippets = useSnippets(ref)
const { mutateAsync: formatQuery } = useFormatQueryMutation()
useContentIdQuery(
{ projectRef: ref, id },
{ enabled: enableFolders, onSuccess: (data) => console.log(data) }
)
const [intellisenseEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
true
+102 -13
View File
@@ -484,7 +484,7 @@ export interface paths {
/** Deletes project's contents */
delete: operations['ContentController_deleteContents']
}
'/platform/projects/{ref}/content/{id}': {
'/platform/projects/{ref}/content/item/{id}': {
/** Gets project's content by the given id */
get: operations['ContentController_getContentById']
}
@@ -1359,7 +1359,7 @@ export interface paths {
/** Deletes project's contents */
delete: operations['ContentController_deleteContents']
}
'/v0/projects/{ref}/content/{id}': {
'/v0/projects/{ref}/content/item/{id}': {
/** Gets project's content by the given id */
get: operations['ContentController_getContentById']
}
@@ -1937,17 +1937,21 @@ export interface paths {
post: operations['ExtensionsController_provisionResource']
}
'/partners/flyio/organizations/{organization_id}': {
/** Gets information about the organization */
/** Gets details of the organization linked to the provided Fly organization id */
get: operations['OrganizationsController_getOrganization']
}
'/partners/flyio/organizations/{organization_id}/extensions': {
/** Gets all databases that belong to the Fly organization */
/** Gets all databases that belong to the given Fly organization id */
get: operations['OrganizationsController_getOrgExtensions']
}
'/partners/flyio/organizations/{organization_id}/sso': {
/** Starts Fly single sign on */
get: operations['OrganizationsController_startFlyioSSO']
}
'/partners/flyio/organizations/{organization_id}/subscription': {
/** Updates organization subscription linked to the provided Fly organization id */
put: operations['OrganizationsController_updateOrganization']
}
'/partners/flyio/organizations/{organization_id}/billing': {
/** Gets the organizations current unbilled charges */
get: operations['FlyBillingController_getResourceBilling']
@@ -2122,6 +2126,7 @@ export interface components {
SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION: boolean
SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: number
MAILER_OTP_EXP: number
MAILER_OTP_LENGTH: number
SMS_AUTOCONFIRM: boolean
SMS_MAX_FREQUENCY: number
SMS_OTP_EXP: number
@@ -2276,6 +2281,7 @@ export interface components {
SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION?: boolean
SECURITY_REFRESH_TOKEN_REUSE_INTERVAL?: number
MAILER_OTP_EXP?: number
MAILER_OTP_LENGTH?: number
SMS_AUTOCONFIRM?: boolean
SMS_MAX_FREQUENCY?: number
SMS_OTP_EXP?: number
@@ -3637,7 +3643,10 @@ export interface components {
GetProjectByFlyExtensionIdResponse: {
ref: string
}
/** @enum {string} */
/**
* @description Desired instance size, will use default size if not defined. Paid plans only.
* @enum {string}
*/
DesiredInstanceSize:
| 'micro'
| 'small'
@@ -4149,10 +4158,11 @@ export interface components {
db_schema?: string
}
V1PostgrestConfigResponse: {
max_rows: number | null
max_rows: number
/** @description If `null`, the value is automatically configured based on compute size. */
db_pool: number | null
db_schema: string | null
db_extra_search_path: string | null
db_schema: string
db_extra_search_path: string
}
PostgresConfigResponse: {
statement_timeout?: string
@@ -4163,6 +4173,8 @@ export interface components {
max_parallel_maintenance_workers?: number
max_parallel_workers?: number
max_parallel_workers_per_gather?: number
max_standby_archive_delay?: string
max_standby_streaming_delay?: string
max_worker_processes?: number
shared_buffers?: string
work_mem?: string
@@ -4178,6 +4190,8 @@ export interface components {
max_parallel_maintenance_workers?: number
max_parallel_workers?: number
max_parallel_workers_per_gather?: number
max_standby_archive_delay?: string
max_standby_streaming_delay?: string
max_worker_processes?: number
shared_buffers?: string
work_mem?: string
@@ -4832,6 +4846,7 @@ export interface components {
/** @enum {string} */
tier: 'tier_payg' | 'tier_pro' | 'tier_free' | 'tier_team' | 'tier_enterprise'
price_id?: string
skip_free_plan_validations?: boolean
}
RestrictionData: {
grace_period_end?: string
@@ -5072,10 +5087,11 @@ export interface components {
root_key: string
}
PostgrestConfigWithJWTSecretResponse: {
max_rows: number | null
max_rows: number
/** @description If `null`, the value is automatically configured based on compute size. */
db_pool: number | null
db_schema: string | null
db_extra_search_path: string | null
db_schema: string
db_extra_search_path: string
jwt_secret?: string
}
V1ProjectRefResponse: {
@@ -5300,6 +5316,7 @@ export interface components {
mailer_allow_unverified_email_sign_ins: boolean | null
mailer_autoconfirm: boolean | null
mailer_otp_exp: number | null
mailer_otp_length: number | null
mailer_secure_email_change_enabled: boolean | null
mailer_subjects_confirmation: string | null
mailer_subjects_email_change: string | null
@@ -5423,6 +5440,7 @@ export interface components {
security_update_password_require_reauthentication?: boolean
security_refresh_token_reuse_interval?: number
mailer_otp_exp?: number
mailer_otp_length?: number
sms_autoconfirm?: boolean
sms_max_frequency?: number
sms_otp_exp?: number
@@ -5788,6 +5806,47 @@ export interface components {
/** @description Supabase project services health status */
services: components['schemas']['ServiceHealthResponse'][]
}
/**
* @description Organization subscription plan
* @enum {string}
*/
SelfServePlanId: 'free' | 'pro' | 'team'
FlyResourceProvisioningBody: {
/** @description A UNIX epoch timestamp value */
timestamp: number
/** @description A random unique string identifying the individual request */
nonce: string
/** @description The full request target URL */
url: string
/** @description Name of the extension */
name: string
/** @description Unique ID representing the extension */
id: string
/** @description Unique ID representing an organization */
organization_id: string
/** @description Display name for an organization */
organization_name: string
/** @description Obfuscated email that routes to the provisioning user */
user_email: string
/** @description Unique ID representing an user */
user_id: string
/** @description The three-letter, primary Fly.io region where the target app intends to write from */
primary_region: string
/** @description An IPv6 address on the customer network assigned to this extension */
ip_address: string
/**
* @description An array of Fly.io region codes where read replicas should be provisioned
* @default []
*/
read_regions: string[]
/** @description Database password (Optional, don't send to generate one) */
db_pass?: string
/** @example large */
desired_instance_size?: components['schemas']['DesiredInstanceSize']
/** @example pro */
organization_plan?: components['schemas']['SelfServePlanId']
user_name: string
}
ResourceProvisioningConfigResponse: {
/**
* @description PSQL connection string
@@ -5855,6 +5914,10 @@ export interface components {
*/
supabase_org_id: string
}
UpdateFlyOrganizationSubscriptionBody: {
/** @example pro */
plan: components['schemas']['SelfServePlanId']
}
ResourceBillingItem: {
/**
* @description Non-Unique identifier of the item
@@ -9477,6 +9540,9 @@ export interface operations {
/** Gets project's content root folder */
ContentFoldersController_getRootFolder: {
parameters: {
query?: {
type?: 'sql' | 'report' | 'log_sql'
}
path: {
/** @description Project ref */
ref: string
@@ -14438,6 +14504,11 @@ export interface operations {
}
/** Creates a database */
ExtensionsController_provisionResource: {
requestBody: {
content: {
'application/json': components['schemas']['FlyResourceProvisioningBody']
}
}
responses: {
201: {
content: {
@@ -14446,7 +14517,7 @@ export interface operations {
}
}
}
/** Gets information about the organization */
/** Gets details of the organization linked to the provided Fly organization id */
OrganizationsController_getOrganization: {
parameters: {
path: {
@@ -14461,7 +14532,7 @@ export interface operations {
}
}
}
/** Gets all databases that belong to the Fly organization */
/** Gets all databases that belong to the given Fly organization id */
OrganizationsController_getOrgExtensions: {
parameters: {
path: {
@@ -14489,6 +14560,24 @@ export interface operations {
}
}
}
/** Updates organization subscription linked to the provided Fly organization id */
OrganizationsController_updateOrganization: {
parameters: {
path: {
organization_id: string
}
}
requestBody: {
content: {
'application/json': components['schemas']['UpdateFlyOrganizationSubscriptionBody']
}
}
responses: {
200: {
content: never
}
}
}
/** Gets the organizations current unbilled charges */
FlyBillingController_getResourceBilling: {
parameters: {