mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
started migrating components to shadcn. also experimented with loading icon
This commit is contained in:
1 parent
08ed506156
commit
a08084c839
16 files changed
+1399
-340
No files matched your search
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Do not edit directly
|
||||
* Generated on Mon, 17 Jul 2023 15:59:23 GMT
|
||||
* Generated on Fri, 28 Jul 2023 11:07:13 GMT
|
||||
*/
|
||||
|
||||
:root {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/**
|
||||
* Do not edit directly
|
||||
* Generated on Mon, 17 Jul 2023 15:59:23 GMT
|
||||
* Generated on Fri, 28 Jul 2023 11:07:13 GMT
|
||||
*/
|
||||
|
||||
.dark {
|
||||
--brand-default: 152.9deg 60% 52.9%;
|
||||
--brand-400: 153.6deg 41% 12%;
|
||||
--brand-300: 152.7deg 48.5% 26.7%;
|
||||
--brand-200: 148.5deg 43.5% 82%;
|
||||
--brand-600: 152.9deg 60% 50%;
|
||||
--brand-500: 153.5deg 61.8% 21.6%;
|
||||
--brand-400: 153.3deg 65.2% 13.5%;
|
||||
--brand-300: 153.8deg 69.6% 9%;
|
||||
--brand-200: 152.5deg 75% 6.3%;
|
||||
--warning-default: 38.9deg 100% 57.1%;
|
||||
--warning-600: 38.9deg 89.8% 49.8%;
|
||||
--warning-500: 34.8deg 90.9% 21.6%;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/**
|
||||
* Do not edit directly
|
||||
* Generated on Mon, 17 Jul 2023 15:59:23 GMT
|
||||
* Generated on Fri, 28 Jul 2023 11:07:13 GMT
|
||||
*/
|
||||
|
||||
.light {
|
||||
--brand-primary: 152.9deg 60% 52.9%;
|
||||
--brand-400: 153.7deg 68.7% 83.7%;
|
||||
--brand-300: 153deg 46% 65.9%;
|
||||
--brand-200: 148.5deg 43.5% 82%;
|
||||
--brand-default: 152.9deg 60% 52.9%;
|
||||
--brand-600: 153.2deg 49.7% 33.5%;
|
||||
--brand-500: 148.7deg 42.7% 69.2%;
|
||||
--brand-400: 149.1deg 59.3% 88.4%;
|
||||
--brand-300: 148.6deg 77.8% 94.7%;
|
||||
--brand-200: 156deg 71.4% 98.6%;
|
||||
--warning-default: 38.9deg 100% 57.1%;
|
||||
--warning-600: 30.2deg 100% 33.9%;
|
||||
--warning-500: 36.3deg 85.7% 67.1%;
|
||||
|
||||
@@ -529,15 +529,23 @@ module.exports = {
|
||||
},
|
||||
"brand-200": {
|
||||
cssVariable: "var(--brand-200)",
|
||||
value: "hsl(148.5, 43.5%, 82%)"
|
||||
value: "hsl(152.5, 75%, 6.3%)"
|
||||
},
|
||||
"brand-300": {
|
||||
cssVariable: "var(--brand-300)",
|
||||
value: "hsl(152.70000000000005, 48.5%, 26.7%)"
|
||||
value: "hsl(153.79999999999995, 69.6%, 9%)"
|
||||
},
|
||||
"brand-400": {
|
||||
cssVariable: "var(--brand-400)",
|
||||
value: "hsl(153.60000000000002, 41%, 12%)"
|
||||
value: "hsl(153.29999999999995, 65.2%, 13.5%)"
|
||||
},
|
||||
"brand-500": {
|
||||
cssVariable: "var(--brand-500)",
|
||||
value: "hsl(153.5, 61.8%, 21.6%)"
|
||||
},
|
||||
"brand-600": {
|
||||
cssVariable: "var(--brand-600)",
|
||||
value: "hsl(152.89999999999998, 60%, 50%)"
|
||||
},
|
||||
"brand-DEFAULT": {
|
||||
cssVariable: "var(--brand-default)",
|
||||
|
||||
@@ -119,6 +119,8 @@ export {
|
||||
AlertDescription as AlertDescription_Shadcn_,
|
||||
} from './src/components/shadcn/ui/alert'
|
||||
|
||||
export { Input as Input_Shadcn_ } from './src/components/shadcn/ui/input'
|
||||
|
||||
export {
|
||||
useFormField as useFormField_Shadcn_,
|
||||
Form as Form_Shadcn_,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from 'src/lib/utils'
|
||||
import { cn } from '@ui/lib/utils'
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
@@ -12,7 +12,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
...customClasses,
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -41,6 +41,8 @@ export default {
|
||||
"bg-brand-200",
|
||||
"bg-brand-300",
|
||||
"bg-brand-400",
|
||||
"bg-brand-500",
|
||||
"bg-brand-600",
|
||||
"bg-brand"
|
||||
],
|
||||
"palletes": [
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// make a function that returns a component
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
const AiIconAnimation = () => {
|
||||
const [step, setStep] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setStep((step) => {
|
||||
return (step % 5) + 1
|
||||
})
|
||||
}, 2000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="ai-icon__container animate--[spin_5s_ease-in-out_infinite] spin-ai-icon-container">
|
||||
<div className="ai-icon__grid">
|
||||
<div className={`ai-icon__grid__square ai-icon__grid__square--step-${step}`}></div>
|
||||
<div className={`ai-icon__grid__square ai-icon__grid__square--step-${step}`}></div>
|
||||
<div className={`ai-icon__grid__square ai-icon__grid__square--step-${step}`}></div>
|
||||
<div className={`ai-icon__grid__square ai-icon__grid__square--step-${step}`}></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { AiIconAnimation }
|
||||
@@ -4,6 +4,7 @@ import { editor } from 'monaco-editor'
|
||||
import { MutableRefObject, useRef, useState } from 'react'
|
||||
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
|
||||
import { IStandaloneCodeEditor } from './SQLEditor.types'
|
||||
import { cn } from 'ui'
|
||||
|
||||
export type MonacoEditorProps = {
|
||||
id: string
|
||||
@@ -11,6 +12,7 @@ export type MonacoEditorProps = {
|
||||
isExecuting: boolean
|
||||
autoFocus?: boolean
|
||||
executeQuery: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const MonacoEditor = ({
|
||||
@@ -18,6 +20,7 @@ const MonacoEditor = ({
|
||||
editorRef,
|
||||
isExecuting,
|
||||
autoFocus = true,
|
||||
className,
|
||||
executeQuery,
|
||||
}: MonacoEditorProps) => {
|
||||
const snap = useSqlEditorStateSnapshot({ sync: true })
|
||||
@@ -73,7 +76,7 @@ const MonacoEditor = ({
|
||||
return (
|
||||
<>
|
||||
<Editor
|
||||
className="monaco-editor"
|
||||
className={cn(className, 'monaco-editor')}
|
||||
theme={'supabase'}
|
||||
onMount={handleEditorOnMount}
|
||||
onChange={handleEditorChange}
|
||||
|
||||
@@ -45,6 +45,8 @@ import {
|
||||
IconSettings,
|
||||
IconX,
|
||||
Input,
|
||||
Input_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import AISettingsModal from './AISettingsModal'
|
||||
import {
|
||||
@@ -55,6 +57,7 @@ import {
|
||||
import { IStandaloneCodeEditor, IStandaloneDiffEditor } from './SQLEditor.types'
|
||||
import { createSqlSnippetSkeleton } from './SQLEditor.utils'
|
||||
import UtilityPanel from './UtilityPanel/UtilityPanel'
|
||||
import { AiIconAnimation } from 'components/animations/ai-icon'
|
||||
|
||||
// Load the monaco editor client-side only (does not behave well server-side)
|
||||
const MonacoEditor = dynamic(() => import('./MonacoEditor'), { ssr: false })
|
||||
@@ -136,6 +139,8 @@ const SQLEditor = () => {
|
||||
const [sqlDiff, setSqlDiff] = useState<ContentDiff>()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const [aiOpen, setAiOpen] = useState(false)
|
||||
|
||||
const [isAISettingsOpen, setIsAISettingsOpen] = useState(false)
|
||||
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false)
|
||||
|
||||
@@ -417,248 +422,333 @@ const SQLEditor = () => {
|
||||
executeQuery(true)
|
||||
}}
|
||||
/>
|
||||
<div className="flex h-full flex-col">
|
||||
<motion.div
|
||||
key="ask-ai-input-container"
|
||||
layoutId="ask-ai-input-container"
|
||||
variants={{
|
||||
visible: {
|
||||
borderRadius: 0,
|
||||
},
|
||||
}}
|
||||
initial="visible"
|
||||
animate="visible"
|
||||
className="w-full flex justify-center z-10 mt-0.5 bg-brand-400"
|
||||
>
|
||||
<AnimatePresence initial={false} exitBeforeEnter>
|
||||
{!isEditSqlLoading ? (
|
||||
<motion.div
|
||||
key="ask-ai-input"
|
||||
className="w-full"
|
||||
variants={{
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
},
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: -25,
|
||||
},
|
||||
}}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="hidden"
|
||||
transition={{
|
||||
duration: 0.1,
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
autoFocus
|
||||
size="xlarge"
|
||||
value={aiInput}
|
||||
onChange={(e) => setAiInput(e.currentTarget.value)}
|
||||
disabled={isDiffOpen}
|
||||
inputRef={inputRef}
|
||||
icon={
|
||||
<div className="h-full flex flex-row gap-3 items-center">
|
||||
<motion.div layoutId="ask-ai-input-icon" className="ml-1">
|
||||
<AiIcon className="w-4 h-4" />
|
||||
</motion.div>
|
||||
<div className="flex h-full flex-col relative">
|
||||
{aiOpen && (
|
||||
<motion.div
|
||||
key="ask-ai-input-container"
|
||||
layoutId="ask-ai-input-container"
|
||||
variants={{
|
||||
visible: {
|
||||
borderRadius: 0,
|
||||
},
|
||||
}}
|
||||
initial="visible"
|
||||
animate="visible"
|
||||
className="w-full flex justify-center z-10 h-[60px] bg-brand-200 border-b border-brand-400 px-5"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'w-full !border-brand-900 border-none !shadow-none placeholder:text-scale-900',
|
||||
'flex items-center gap-3'
|
||||
)}
|
||||
>
|
||||
{/* <div className="group h-12 w-12 relative group"> */}
|
||||
{/* <div className="absolute duration-2000 h-12 w-12 skew--y-[24deg]">
|
||||
<div
|
||||
className={cn(
|
||||
'transition-all',
|
||||
'absolute top-3 left-3 h-4 w-4 border border-brand',
|
||||
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-75'
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
'transition-all',
|
||||
'absolute top-3 left-5 h-4 w-4 border border-brand',
|
||||
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-200'
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
'transition-all',
|
||||
'absolute top-5 left-3 h-4 w-4 border border-brand',
|
||||
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-400'
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
'transition-all',
|
||||
'absolute top-5 left-5 h-4 w-4 border border-brand',
|
||||
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-600'
|
||||
)}
|
||||
></div>
|
||||
</div> */}
|
||||
|
||||
{debugSolution && (
|
||||
<div className="h-full w-full flex flex-row items-center overflow-y-hidden mr-[16.5rem] text-sm text-white">
|
||||
{debugSolution}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
inputClassName="w-full !border-brand-900 border-none bg-transparent !shadow-none py-4 focus:!ring-0 placeholder:text-scale-900 pr-20"
|
||||
iconContainerClassName="transition text-scale-800 text-brand-900"
|
||||
placeholder={!debugSolution ? 'Ask Supabase AI to modify your query' : ''}
|
||||
className="w-full"
|
||||
actions={
|
||||
<div className="flex flex-row items-center gap-2 space-x-1 mr-6">
|
||||
{isDiffOpen ? (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="rounded-r-none"
|
||||
type="primary"
|
||||
size="tiny"
|
||||
icon={
|
||||
!isAcceptDiffLoading ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
)
|
||||
}
|
||||
iconRight={
|
||||
<div className="opacity-30">
|
||||
<IconCornerDownLeft size={12} strokeWidth={1.5} />
|
||||
</div>
|
||||
}
|
||||
onClick={acceptAiHandler}
|
||||
>
|
||||
{getDiffTypeButtonLabel(selectedDiffType)}
|
||||
</Button>
|
||||
<Dropdown
|
||||
align="end"
|
||||
side="bottom"
|
||||
overlay={Object.values(DiffType)
|
||||
.filter((diffType) => diffType !== selectedDiffType)
|
||||
.map((diffType) => (
|
||||
<Dropdown.Item
|
||||
key={diffType}
|
||||
onClick={() => {
|
||||
setSelectedDiffType(diffType)
|
||||
switch (diffType) {
|
||||
case DiffType.Modification:
|
||||
return compareAsModification()
|
||||
case DiffType.Addition:
|
||||
return compareAsAddition()
|
||||
case DiffType.NewSnippet:
|
||||
return compareAsNewSnippet()
|
||||
default:
|
||||
throw new Error(`Unknown diff type '${diffType}'`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{getDiffTypeDropdownLabel(diffType)}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
className="rounded-l-none border-l-0 px-[4px] py-[5px]"
|
||||
icon={<IconChevronDown />}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<Button
|
||||
type="alternative"
|
||||
size="tiny"
|
||||
icon={<IconX />}
|
||||
iconRight={<span className="opacity-30">ESC</span>}
|
||||
onClick={discardAiHandler}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconCornerDownLeft size={16} strokeWidth={1.5} />
|
||||
<IconSettings
|
||||
className="cursor-pointer"
|
||||
onClick={() => setIsAISettingsOpen(true)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
onKeyPress={async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
try {
|
||||
const prompt = e.currentTarget.value
|
||||
|
||||
if (!prompt) {
|
||||
return
|
||||
}
|
||||
|
||||
const sql = editorRef.current?.getValue()
|
||||
|
||||
if (!sql) {
|
||||
return
|
||||
}
|
||||
|
||||
const { sql: modifiedSql } = await editSql({
|
||||
prompt,
|
||||
sql: sql.replace(sqlAiDisclaimerComment, '').trim(),
|
||||
entityDefinitions,
|
||||
})
|
||||
|
||||
const formattedSql =
|
||||
sqlAiDisclaimerComment +
|
||||
'\n\n' +
|
||||
format(modifiedSql, {
|
||||
language: 'postgresql',
|
||||
keywordCase: 'lower',
|
||||
})
|
||||
|
||||
// TODO: show error
|
||||
if (formattedSql.trim() === sql.trim()) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: 'Unable to edit SQL. Try adding more details to your prompt.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setSqlDiff({
|
||||
original: sql,
|
||||
modified: formattedSql,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
if (isError(error)) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: error.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="ask-ai-loading"
|
||||
className="p-4 flex flex-row gap-2 items-center w-full"
|
||||
variants={{
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
},
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 25,
|
||||
},
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
}}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="hidden"
|
||||
>
|
||||
<motion.div
|
||||
className="text-brand-900"
|
||||
animate={{
|
||||
scale: [0.9, 1.1, 0.9],
|
||||
transition: {
|
||||
ease: 'linear',
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AiIcon className="w-4 h-4" />
|
||||
{/* <div className="absolute top-0.5 right-0.5 duration-2000 h-12 w-12 skew-y-[24deg] hover:top-0 hover:right-0">
|
||||
<div
|
||||
className={cn(
|
||||
'transition-all',
|
||||
'absolute top-3 left-3 h-4 w-4 border border-brand',
|
||||
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-75'
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
'transition-all',
|
||||
'absolute top-3 left-5 h-4 w-4 border border-brand',
|
||||
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-200'
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
'transition-all',
|
||||
'absolute top-5 left-3 h-4 w-4 border border-brand',
|
||||
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-400'
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
'transition-all',
|
||||
'absolute top-5 left-5 h-4 w-4 border border-brand',
|
||||
'group-hover:top-4 group-hover:left-4 group-hover:rotate--0 group-hover:skew-x--12 duration-200 delay-600'
|
||||
)}
|
||||
></div>
|
||||
</div> */}
|
||||
{/* </div> */}
|
||||
{/* <div className="flex flex-row gap-3 items-center">
|
||||
<motion.div layoutId="ask-ai-input-icon">
|
||||
<AiIcon className="w-4 h-4 text-brand" />
|
||||
</motion.div>
|
||||
<motion.span
|
||||
animate={{
|
||||
opacity: ['0.5', '0.75', '0.5'],
|
||||
transition: {
|
||||
ease: 'linear',
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Thinking...
|
||||
</motion.span>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</div> */}
|
||||
|
||||
<AiIconAnimation />
|
||||
|
||||
<AnimatePresence initial={false} exitBeforeEnter>
|
||||
{debugSolution && (
|
||||
<div className="h-full w-full flex flex-row items-center overflow-y-hidden text-sm text-white">
|
||||
{debugSolution}
|
||||
</div>
|
||||
)}
|
||||
{!isEditSqlLoading ? (
|
||||
<motion.div
|
||||
key="ask-ai-input"
|
||||
className="w-full h-full relative flex items-center"
|
||||
variants={{
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
},
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: -25,
|
||||
},
|
||||
}}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="hidden"
|
||||
transition={{
|
||||
duration: 0.1,
|
||||
}}
|
||||
>
|
||||
<Input_Shadcn_
|
||||
autoFocus
|
||||
value={aiInput}
|
||||
onChange={(e) => setAiInput(e.currentTarget.value)}
|
||||
disabled={isDiffOpen}
|
||||
ref={inputRef}
|
||||
className={cn(
|
||||
'!p-0 bg-transparent border-transparent text-sm text-brand placeholder:text-brand-600 focus:!ring-0',
|
||||
'focus-visible:ring-0 focus-visible:ring-offset-0',
|
||||
'appearance-none outline-none'
|
||||
// "after:content-['_↗']",
|
||||
// 'after:relative after:w-10 after:h-10',
|
||||
// 'after:placeholder:content-["hello"]'
|
||||
)}
|
||||
placeholder={!debugSolution ? 'Ask Supabase AI to modify your query' : ''}
|
||||
onKeyPress={async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
console.log('entering')
|
||||
try {
|
||||
const prompt = e.currentTarget.value
|
||||
|
||||
if (!prompt) {
|
||||
return
|
||||
}
|
||||
|
||||
const sql = editorRef.current?.getValue()
|
||||
|
||||
if (!sql) {
|
||||
return
|
||||
}
|
||||
|
||||
const { sql: modifiedSql } = await editSql({
|
||||
prompt,
|
||||
sql: sql.replace(sqlAiDisclaimerComment, '').trim(),
|
||||
entityDefinitions,
|
||||
})
|
||||
|
||||
const formattedSql =
|
||||
sqlAiDisclaimerComment +
|
||||
'\n\n' +
|
||||
format(modifiedSql, {
|
||||
language: 'postgresql',
|
||||
keywordCase: 'lower',
|
||||
})
|
||||
|
||||
// TODO: show error
|
||||
if (formattedSql.trim() === sql.trim()) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message:
|
||||
'Unable to edit SQL. Try adding more details to your prompt.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setSqlDiff({
|
||||
original: sql,
|
||||
modified: formattedSql,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
'message' in error &&
|
||||
typeof error.message === 'string'
|
||||
) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: error.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* <div className="opacity-30">
|
||||
<IconCornerDownLeft size={12} strokeWidth={1.5} />
|
||||
</div> */}
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="ask-ai-loading"
|
||||
className="p-0 flex flex-row gap-2 items-center w-full"
|
||||
variants={{
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
},
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 25,
|
||||
},
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
}}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="hidden"
|
||||
>
|
||||
<motion.span
|
||||
className="text-sm text-brand px-3"
|
||||
animate={{
|
||||
opacity: ['0.5', '0.75', '0.5'],
|
||||
transition: {
|
||||
ease: 'linear',
|
||||
duration: 0.33,
|
||||
repeat: Infinity,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Thinking...
|
||||
</motion.span>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{isDiffOpen ? (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="rounded-r-none"
|
||||
type="primary"
|
||||
size="tiny"
|
||||
icon={
|
||||
!isAcceptDiffLoading ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
)
|
||||
}
|
||||
iconRight={
|
||||
<div className="opacity-30">
|
||||
<IconCornerDownLeft size={12} strokeWidth={1.5} />
|
||||
</div>
|
||||
}
|
||||
onClick={acceptAiHandler}
|
||||
>
|
||||
{getDiffTypeButtonLabel(selectedDiffType)}
|
||||
</Button>
|
||||
<Dropdown
|
||||
align="end"
|
||||
side="bottom"
|
||||
overlay={Object.values(DiffType)
|
||||
.filter((diffType) => diffType !== selectedDiffType)
|
||||
.map((diffType) => (
|
||||
<Dropdown.Item
|
||||
key={diffType}
|
||||
onClick={() => {
|
||||
setSelectedDiffType(diffType)
|
||||
switch (diffType) {
|
||||
case DiffType.Modification:
|
||||
return compareAsModification()
|
||||
case DiffType.Addition:
|
||||
return compareAsAddition()
|
||||
case DiffType.NewSnippet:
|
||||
return compareAsNewSnippet()
|
||||
default:
|
||||
throw new Error(`Unknown diff type '${diffType}'`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{getDiffTypeDropdownLabel(diffType)}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
className="rounded-l-none border-l-0 px-[4px] py-[5px]"
|
||||
icon={<IconChevronDown />}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<Button
|
||||
type="alternative"
|
||||
size="tiny"
|
||||
icon={<IconX />}
|
||||
iconRight={<span className="opacity-30">ESC</span>}
|
||||
onClick={discardAiHandler}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={cn('transition text-brand', !aiInput ? 'opacity-0' : 'opacity-30')}
|
||||
>
|
||||
<IconCornerDownLeft size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="text-brand hover:text-brand-600"
|
||||
onClick={() => setAiOpen(false)}
|
||||
>
|
||||
<IconX size={21} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsAISettingsOpen(true)}
|
||||
className="text-lighter hover:text transition"
|
||||
>
|
||||
<IconSettings className="cursor-pointer" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
<Split
|
||||
style={{ height: '100%' }}
|
||||
direction="vertical"
|
||||
@@ -672,7 +762,33 @@ const SQLEditor = () => {
|
||||
collapsed={isUtilityPanelCollapsed ? 1 : undefined}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<div className="dark:border-dark flex-grow overflow-y-auto border-b">
|
||||
<div className="flex-grow overflow-y-auto border-b">
|
||||
{!aiOpen && (
|
||||
<button
|
||||
onClick={() => setAiOpen(!aiOpen)}
|
||||
className={cn(
|
||||
'group',
|
||||
'absolute z-10',
|
||||
'rounded-lg',
|
||||
'right-5 top-1.5',
|
||||
'w-10 h-10 bg-brand-400 border border-brand-600 shadow-sm',
|
||||
'transition-all duration-200',
|
||||
'ease-out hover:w-[155px]',
|
||||
'flex items-center justify-center group-hover:justify-between gap-3'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'hidden text-brand w-0 ',
|
||||
'group-hover:w-auto group-hover:inline-block truncate'
|
||||
)}
|
||||
>
|
||||
Supabase AI
|
||||
</span>
|
||||
<AiIcon className="text-brand w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { partition } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { useParams, useTelemetryProps } from 'common'
|
||||
import {
|
||||
SQL_TEMPLATES,
|
||||
sqlAiDisclaimerComment,
|
||||
} from 'components/interfaces/SQLEditor/SQLEditor.constants'
|
||||
import { useSqlGenerateMutation } from 'data/ai/sql-generate-mutation'
|
||||
import { SqlSnippet } from 'data/content/sql-snippets-query'
|
||||
import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query'
|
||||
import { isError } from 'data/utils/error-check'
|
||||
import { motion } from 'framer-motion'
|
||||
import {
|
||||
useCheckPermissions,
|
||||
useLocalStorageQuery,
|
||||
useSelectedOrganization,
|
||||
useSelectedProject,
|
||||
useStore,
|
||||
} from 'hooks'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import Telemetry from 'lib/telemetry'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import { format } from 'sql-formatter'
|
||||
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
|
||||
import { AiIcon, IconCornerDownLeft, IconSettings, Input } from 'ui'
|
||||
import AISettingsModal from '../AISettingsModal'
|
||||
import { createSqlSnippetSkeleton } from '../SQLEditor.utils'
|
||||
import SQLCard from './SQLCard'
|
||||
|
||||
const SQLQuickstarts = observer(() => {
|
||||
const { ui } = useStore()
|
||||
const { ref } = useParams()
|
||||
const router = useRouter()
|
||||
const { profile } = useProfile()
|
||||
const [sql, quickStart] = partition(SQL_TEMPLATES, { type: 'template' })
|
||||
const { mutateAsync: generateSql, isLoading: isSqlGenerateLoading } = useSqlGenerateMutation()
|
||||
const [isAISettingsOpen, setIsAISettingsOpen] = useState(false)
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const selectedProject = useSelectedProject()
|
||||
const isOptedInToAI =
|
||||
selectedOrganization?.opt_in_tags?.includes('AI_SQL_GENERATOR_OPT_IN') ?? false
|
||||
const [isOptedInToAISchema] = useLocalStorageQuery('supabase_sql-editor-ai-schema', false)
|
||||
|
||||
const includeSchemaMetadata = (isOptedInToAI || !IS_PLATFORM) && isOptedInToAISchema
|
||||
|
||||
const { data } = useEntityDefinitionsQuery(
|
||||
{
|
||||
projectRef: selectedProject?.ref,
|
||||
connectionString: selectedProject?.connectionString,
|
||||
},
|
||||
{ enabled: includeSchemaMetadata }
|
||||
)
|
||||
|
||||
const entityDefinitions = includeSchemaMetadata ? data?.map((def) => def.sql.trim()) : undefined
|
||||
|
||||
const telemetryProps = useTelemetryProps()
|
||||
const snap = useSqlEditorStateSnapshot()
|
||||
const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
|
||||
resource: { type: 'sql', owner_id: profile?.id },
|
||||
subject: { id: profile?.id },
|
||||
})
|
||||
|
||||
// [Joshen TODO] Removed optimistic query creation logic for now, need to figure out
|
||||
// how to do that after using ids as part of the URL
|
||||
const handleNewQuery = async (sql: string, name: string) => {
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
if (!canCreateSQLSnippet) {
|
||||
return ui.setNotification({
|
||||
category: 'info',
|
||||
message: 'Your queries will not be saved as you do not have sufficient permissions',
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const snippet = createSqlSnippetSkeleton({ name, sql, owner_id: profile?.id })
|
||||
const data = { ...snippet, id: uuidv4() }
|
||||
snap.addSnippet(data as SqlSnippet, ref, true)
|
||||
router.push(`/project/${ref}/sql/${data.id}`)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to create new query: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AISettingsModal visible={isAISettingsOpen} onCancel={() => setIsAISettingsOpen(false)} />
|
||||
<div className="block h-full space-y-8 overflow-y-auto p-6">
|
||||
<div className="mt-32 mb-32 flex flex-col items-center">
|
||||
<motion.h1
|
||||
className="text-scale-1200 mb-8 text-3xl"
|
||||
variants={{
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
hidden: {
|
||||
y: 10,
|
||||
opacity: 0,
|
||||
},
|
||||
}}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
What do you want to build?
|
||||
</motion.h1>
|
||||
<div className="w-full flex justify-center">
|
||||
{!isSqlGenerateLoading ? (
|
||||
<motion.div
|
||||
key="ask-ai-input-container"
|
||||
layoutId="ask-ai-input-container"
|
||||
className="w-full max-w-2xl border border-brand-900"
|
||||
variants={{
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
borderRadius: 6,
|
||||
},
|
||||
hidden: {
|
||||
y: -50,
|
||||
opacity: 0,
|
||||
borderRadius: 6,
|
||||
},
|
||||
}}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<Input
|
||||
size="xlarge"
|
||||
inputRef={(inputElement: HTMLInputElement) => {
|
||||
setTimeout(() => {
|
||||
inputElement?.focus()
|
||||
}, 200)
|
||||
}}
|
||||
icon={
|
||||
<motion.div
|
||||
key="ask-ai-input-icon"
|
||||
layoutId="ask-ai-input-icon"
|
||||
className="ml-1"
|
||||
variants={{
|
||||
visible: {
|
||||
scale: 1,
|
||||
},
|
||||
}}
|
||||
initial="visible"
|
||||
animate="visible"
|
||||
>
|
||||
<AiIcon className="w-4 h-4" />
|
||||
</motion.div>
|
||||
}
|
||||
inputClassName="w-full border-none py-4 focus:!ring-0 pr-20"
|
||||
iconContainerClassName="transition text-scale-800 text-brand-900"
|
||||
placeholder="Ask Supabase AI to build a query"
|
||||
className="w-full"
|
||||
onKeyPress={async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
try {
|
||||
const value = e.currentTarget.value
|
||||
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
|
||||
const { title, sql } = await generateSql({
|
||||
prompt: e.currentTarget.value,
|
||||
entityDefinitions,
|
||||
})
|
||||
|
||||
const formattedSql =
|
||||
sqlAiDisclaimerComment +
|
||||
'\n\n' +
|
||||
format(sql, {
|
||||
language: 'postgresql',
|
||||
keywordCase: 'lower',
|
||||
})
|
||||
|
||||
await handleNewQuery(formattedSql, title)
|
||||
} catch (error: unknown) {
|
||||
if (isError(error)) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: error.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
actions={
|
||||
<div className="flex items-center space-x-1 mr-4 gap-2">
|
||||
<IconCornerDownLeft size={16} strokeWidth={1.5} />
|
||||
<IconSettings
|
||||
className="cursor-pointer"
|
||||
onClick={() => setIsAISettingsOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="ask-ai-loading"
|
||||
layoutId="ask-ai-input-container"
|
||||
className="p-5 border border-brand-900 text-brand-900"
|
||||
variants={{
|
||||
visible: {
|
||||
borderRadius: 50,
|
||||
},
|
||||
}}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
mass: 0.1,
|
||||
stiffness: 200,
|
||||
damping: 30,
|
||||
}}
|
||||
initial="visible"
|
||||
animate="visible"
|
||||
>
|
||||
<motion.div
|
||||
key="ask-ai-loading-icon"
|
||||
layoutId="ask-ai-input-icon"
|
||||
animate={{
|
||||
scale: [0.9, 1.1, 0.9],
|
||||
transition: {
|
||||
delay: 0.2,
|
||||
ease: 'linear',
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AiIcon className="w-4 h-4" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<div className="mb-4">
|
||||
<h1 className="text-scale-1200 mb-3 text-xl">Quick start</h1>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
While we're in beta, we want to offer a quick way to explore Supabase. While we build
|
||||
importers, check out these simple starters.
|
||||
</p>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Click on any script to fill the query box, modify the script, then click
|
||||
<span className="text-code">Run</span>.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{quickStart.map((x) => (
|
||||
<SQLCard
|
||||
key={x.title}
|
||||
title={x.title}
|
||||
description={x.description}
|
||||
sql={x.sql}
|
||||
onClick={(sql, title) => {
|
||||
handleNewQuery(sql, title)
|
||||
Telemetry.sendEvent(
|
||||
{
|
||||
category: 'quickstart',
|
||||
action: 'quickstart_clicked',
|
||||
label: x.title,
|
||||
},
|
||||
telemetryProps,
|
||||
router
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default SQLQuickstarts
|
||||
@@ -273,41 +273,6 @@ const SQLTemplates = observer(() => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<div className="mb-4">
|
||||
<h1 className="text-scale-1200 mb-3 text-xl">Quick start</h1>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
While we're in beta, we want to offer a quick way to explore Supabase. While we build
|
||||
importers, check out these simple starters.
|
||||
</p>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Click on any script to fill the query box, modify the script, then click
|
||||
<span className="text-code">Run</span>.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{quickStart.map((x) => (
|
||||
<SQLCard
|
||||
key={x.title}
|
||||
title={x.title}
|
||||
description={x.description}
|
||||
sql={x.sql}
|
||||
onClick={(sql, title) => {
|
||||
handleNewQuery(sql, title)
|
||||
Telemetry.sendEvent(
|
||||
{
|
||||
category: 'quickstart',
|
||||
action: 'quickstart_clicked',
|
||||
label: x.title,
|
||||
},
|
||||
telemetryProps,
|
||||
router
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import SQLTemplates from './SQLTemplates/SQLTemplates'
|
||||
import SQLQuickstarts from './SQLTemplates/SQLQuickstarts'
|
||||
|
||||
export { SQLTemplates }
|
||||
export { SQLTemplates, SQLQuickstarts }
|
||||
@@ -14,7 +14,7 @@ import { useCheckPermissions, useStore } from 'hooks'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { useSnippets, useSqlEditorStateSnapshot } from 'state/sql-editor'
|
||||
import { IconSearch, IconX, Input, Menu, cn } from 'ui'
|
||||
import { Button, IconSearch, IconX, Input, Menu, cn } from 'ui'
|
||||
import QueryItem from './QueryItem'
|
||||
|
||||
const SideBarContent = observer(() => {
|
||||
@@ -98,20 +98,31 @@ const SideBarContent = observer(() => {
|
||||
) : isSuccess ? (
|
||||
<div className="space-y-6">
|
||||
<div className="px-3 flex flex-col gap-2">
|
||||
<ProductMenuItem
|
||||
name="Build a query"
|
||||
isActive={id === undefined}
|
||||
url={`/project/${ref}/sql`}
|
||||
/>
|
||||
<ProductMenuItem
|
||||
name="New empty query"
|
||||
isActive={false}
|
||||
<Button
|
||||
// isActive={false}
|
||||
type="default"
|
||||
className="mx-3"
|
||||
onClick={() => {
|
||||
handleNewQuery()
|
||||
}}
|
||||
/>
|
||||
icon={<IconSearch size="tiny" />}
|
||||
>
|
||||
New query
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-6 px-3">
|
||||
<div>
|
||||
<ProductMenuItem
|
||||
name="Templates"
|
||||
isActive={router.asPath === `/project/${ref}/sql`}
|
||||
url={`/project/${ref}/sql`}
|
||||
/>
|
||||
<ProductMenuItem
|
||||
name="Quickstarts"
|
||||
isActive={router.asPath === `/project/${ref}/sql/quickstarts`}
|
||||
url={`/project/${ref}/sql/quickstarts`}
|
||||
/>
|
||||
</div>
|
||||
{favorites.length >= 1 && (
|
||||
<div className="editor-product-menu">
|
||||
<div className="flex flex-row justify-between">
|
||||
@@ -158,52 +169,58 @@ const SideBarContent = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{queries.length >= 1 && (
|
||||
<div className="editor-product-menu">
|
||||
<div className="flex flex-row justify-between">
|
||||
<Menu.Group title="SQL snippets" />
|
||||
|
||||
<div className="editor-product-menu">
|
||||
<div className="flex flex-row justify-between">
|
||||
<Menu.Group title="SQL snippets" />
|
||||
<button
|
||||
className="flex items-center w-4 h-4 cursor-pointer mr-3"
|
||||
onClick={() => {
|
||||
setIsSnippetsFilterOpen((state) => !state)
|
||||
}}
|
||||
>
|
||||
<IconSearch
|
||||
className={cn(
|
||||
'w-4',
|
||||
'h-4',
|
||||
'cursor-pointer',
|
||||
isSnippetsFilterOpen ? 'text-scale-1200' : 'text-scale-900'
|
||||
)}
|
||||
onClick={() => {
|
||||
setIsSnippetsFilterOpen((state) => !state)
|
||||
}}
|
||||
className={cn(isSnippetsFilterOpen ? 'text-scale-1200' : 'text-scale-900')}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{isSnippetsFilterOpen && (
|
||||
<div className="pl-3 mb-2 mr-3">
|
||||
<Input
|
||||
autoFocus
|
||||
size="tiny"
|
||||
icon={<IconSearch size="tiny" />}
|
||||
placeholder="Filter"
|
||||
disabled={isLoading}
|
||||
onChange={(e) => setSnippetsFilterString(e.target.value)}
|
||||
value={snippetsFilterString}
|
||||
actions={
|
||||
snippetsFilterString && (
|
||||
<IconX
|
||||
size={'tiny'}
|
||||
className="mr-2 cursor-pointer"
|
||||
onClick={() => setSnippetsFilterString('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{isSnippetsFilterOpen && (
|
||||
<div className="pl-3 mb-2">
|
||||
<Input
|
||||
autoFocus
|
||||
size="tiny"
|
||||
icon={<IconSearch size="tiny" />}
|
||||
placeholder="Filter"
|
||||
disabled={isLoading}
|
||||
onChange={(e) => setSnippetsFilterString(e.target.value)}
|
||||
value={snippetsFilterString}
|
||||
actions={
|
||||
snippetsFilterString && (
|
||||
<IconX
|
||||
size={'tiny'}
|
||||
className="mr-2 cursor-pointer"
|
||||
onClick={() => setSnippetsFilterString('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{snippetsTabs.length > 0 ? (
|
||||
<div className="space-y-1 pb-8">
|
||||
{snippetsTabs.map((tabInfo) => {
|
||||
const { id } = tabInfo || {}
|
||||
return <QueryItem key={id} tabInfo={tabInfo} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="text text-sm h-32 border border-dashed flex flex-col gap-3 items-center justify-center px-3 mx-3 rounded">
|
||||
<span className="text-lighter">No queries found</span>
|
||||
<Button type="default">New Query</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{snippetsFilterString.length > 0 &&
|
||||
favouriteTabs.length === 0 &&
|
||||
snippetsTabs.length === 0 && (
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { SQLEditorLayout } from 'components/layouts'
|
||||
import { SQLQuickstarts } from 'components/interfaces/SQLEditor'
|
||||
|
||||
const SqlEditorWelcome: NextPageWithLayout = () => {
|
||||
return <SQLQuickstarts />
|
||||
}
|
||||
|
||||
SqlEditorWelcome.getLayout = (page) => <SQLEditorLayout title="Quickstarts">{page}</SQLEditorLayout>
|
||||
|
||||
export default observer(SqlEditorWelcome)
|
||||
@@ -46,3 +46,618 @@ span.th.tr.td.textarea {
|
||||
background: var(--vscode-quickInput-background);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* STEP ONE
|
||||
*/
|
||||
|
||||
@keyframes step1_square1 {
|
||||
0% {
|
||||
transform: rotate(45deg);
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step1_square2 {
|
||||
0% {
|
||||
transform: rotate(45deg);
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
top: 0px;
|
||||
left: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step1_square3 {
|
||||
0% {
|
||||
transform: rotate(45deg);
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
top: 8px;
|
||||
left: 0px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step1_square4 {
|
||||
0% {
|
||||
transform: rotate(45deg);
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* STEP TWO
|
||||
*/
|
||||
|
||||
@keyframes step2_square1 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(45deg);
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step2_square2 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
top: 0px;
|
||||
left: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(45deg);
|
||||
top: 0px;
|
||||
left: 16px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step2_square3 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
top: 8px;
|
||||
left: 0px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(45deg);
|
||||
top: 16px;
|
||||
left: 0px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step2_square4 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(45deg);
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* STEP THREE
|
||||
*/
|
||||
|
||||
@keyframes step3_square1 {
|
||||
0% {
|
||||
transform: rotate(45deg);
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step3_square2 {
|
||||
0% {
|
||||
transform: rotate(45deg);
|
||||
top: 0px;
|
||||
left: 16px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
top: 0px;
|
||||
left: 18px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step3_square3 {
|
||||
0% {
|
||||
transform: rotate(45deg);
|
||||
top: 16px;
|
||||
left: 0px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
top: 18px;
|
||||
left: 0px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step3_square4 {
|
||||
0% {
|
||||
transform: rotate(45deg);
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
top: 18px;
|
||||
left: 18px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* STEP FOUR
|
||||
*/
|
||||
|
||||
@keyframes step4_square1 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
top: 0px;
|
||||
left: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step4_square2 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
top: 0px;
|
||||
left: 18px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
top: 8px;
|
||||
left: 16px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step4_square3 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
top: 18px;
|
||||
left: 0px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
top: 8px;
|
||||
left: 0px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step4_square4 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
top: 18px;
|
||||
left: 18px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
top: 16px;
|
||||
left: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// STEP FIVE
|
||||
|
||||
@keyframes step5_square1 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
top: 0px;
|
||||
left: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(45deg);
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step5_square2 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
top: 8px;
|
||||
left: 16px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(45deg);
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step5_square3 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
top: 8px;
|
||||
left: 0px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(45deg);
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes step5_square4 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
top: 16px;
|
||||
left: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(45deg);
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinAiIconContainer {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
40% {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
// 40% {
|
||||
// transform: rotate(-45deg);
|
||||
// }
|
||||
// 60% {
|
||||
// transform: rotate(90deg); /* 360 degrees * 3 (3 full spins) */
|
||||
// }
|
||||
80% {
|
||||
transform: rotate(180deg); /* 360 degrees * 3 (3 full spins) */
|
||||
}
|
||||
100% {
|
||||
transform: rotate(180deg); /* 360 degrees * 3 (3 full spins) */
|
||||
}
|
||||
// 90% {
|
||||
// transform: rotate(360deg); /* 360 degrees * 5 (5 full spins) */
|
||||
// }
|
||||
// 100% {
|
||||
// transform: rotate(360deg); /* 360 degrees * 5 (5 full spins) */
|
||||
// }
|
||||
}
|
||||
|
||||
.spin-ai-icon-container {
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// background-color: #f00; /* Replace this with your desired background color */
|
||||
// border-radius: 50%;
|
||||
// position: absolute;
|
||||
// animation: spinAiIconContainer 10s infinite;
|
||||
// animation-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
// animation-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
|
||||
/*
|
||||
* STEP SIX
|
||||
*/
|
||||
|
||||
// @keyframes step6_square1 {
|
||||
// 0% {
|
||||
// transform: rotate(45deg);
|
||||
// left: 0px;
|
||||
// top: 0px;
|
||||
// width: 12px;
|
||||
// height: 12px;
|
||||
// }
|
||||
// 100% {
|
||||
// transform: rotate(0deg);
|
||||
// top: 8px;
|
||||
// left: 8px;
|
||||
// width: 8px;
|
||||
// height: 8px;
|
||||
// }
|
||||
// }
|
||||
|
||||
// @keyframes step6_square2 {
|
||||
// 0% {
|
||||
// transform: rotate(45deg);
|
||||
// top: 0px;
|
||||
// left: 8px;
|
||||
// width: 12px;
|
||||
// height: 12px;
|
||||
// }
|
||||
// 100% {
|
||||
// transform: rotate(0deg);
|
||||
// top: 8px;
|
||||
// left: 8px;
|
||||
// width: 8px;
|
||||
// height: 8px;
|
||||
// }
|
||||
// }
|
||||
|
||||
// @keyframes step6_square3 {
|
||||
// 0% {
|
||||
// transform: rotate(45deg);
|
||||
// top: 8px;
|
||||
// left: 0px;
|
||||
// width: 12px;
|
||||
// height: 12px;
|
||||
// }
|
||||
// 100% {
|
||||
// transform: rotate(0deg);
|
||||
// top: 8px;
|
||||
// left: 8px;
|
||||
// width: 8px;
|
||||
// height: 8px;
|
||||
// }
|
||||
// }
|
||||
|
||||
// @keyframes step6_square4 {
|
||||
// 0% {
|
||||
// transform: rotate(45deg);
|
||||
// top: 8px;
|
||||
// left: 8px;
|
||||
// width: 12px;
|
||||
// height: 12px;
|
||||
// }
|
||||
// 100% {
|
||||
// transform: rotate(0deg);
|
||||
// top: 8px;
|
||||
// left: 8px;
|
||||
// width: 8px;
|
||||
// height: 8px;
|
||||
// }
|
||||
// }
|
||||
|
||||
.ai-icon__container {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
// background: red;
|
||||
}
|
||||
|
||||
.ai-icon__grid {
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
// background: yellow;
|
||||
}
|
||||
|
||||
.ai-icon__grid__square {
|
||||
position: absolute;
|
||||
border: 1px solid hsl(var(--brand-default));
|
||||
}
|
||||
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-1:nth-child(1) {
|
||||
animation: step1_square1 700ms forwards;
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
// animation-delay: 0;
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-1:nth-child(2) {
|
||||
animation: step1_square2 700ms forwards;
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
// animation-delay: 200ms;
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-1:nth-child(3) {
|
||||
animation: step1_square3 700ms forwards;
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
// animation-delay: 400ms;
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-1:nth-child(4) {
|
||||
animation: step1_square4 700ms forwards;
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
// animation-delay: 600ms;
|
||||
}
|
||||
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-2:nth-child(1) {
|
||||
animation-delay: 200;
|
||||
animation: step2_square1 0.7s forwards;
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-2:nth-child(2) {
|
||||
animation-delay: 400ms;
|
||||
animation: step2_square2 0.7s forwards;
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-2:nth-child(3) {
|
||||
animation-delay: 600ms;
|
||||
animation: step2_square3 0.7s forwards;
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-2:nth-child(4) {
|
||||
animation-delay: 800ms;
|
||||
animation: step2_square4 0.7s forwards;
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-3:nth-child(1) {
|
||||
animation: step3_square1 0.7s forwards;
|
||||
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-3:nth-child(2) {
|
||||
animation: step3_square2 0.7s forwards;
|
||||
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-3:nth-child(3) {
|
||||
animation: step3_square3 0.7s forwards;
|
||||
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-3:nth-child(4) {
|
||||
animation: step3_square4 0.7s forwards;
|
||||
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-4:nth-child(1) {
|
||||
animation: step4_square1 1s forwards;
|
||||
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-4:nth-child(2) {
|
||||
animation: step4_square2 1s forwards;
|
||||
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-4:nth-child(3) {
|
||||
animation: step4_square3 1s forwards;
|
||||
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-4:nth-child(4) {
|
||||
animation: step4_square4 1s forwards;
|
||||
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
// animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||
}
|
||||
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-5:nth-child(1) {
|
||||
animation: step5_square1 3s forwards;
|
||||
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-5:nth-child(2) {
|
||||
animation: step5_square2 3s forwards;
|
||||
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-5:nth-child(3) {
|
||||
animation: step5_square3 3s forwards;
|
||||
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
.ai-icon__grid .ai-icon__grid__square.ai-icon__grid__square--step-5:nth-child(4) {
|
||||
animation: step5_square4 3s forwards;
|
||||
// animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
Reference in new issue
Block a user