CreateTableInstructions.tsx266 lines · main
1import { useParams } from 'common'
2import { Eye, EyeOff } from 'lucide-react'
3import { useMemo, useState } from 'react'
4import {
5 Accordion,
6 AccordionContent,
7 AccordionItem,
8 AccordionTrigger,
9 Card,
10 CardFooter,
11 CardHeader,
12 CardTitle,
13} from 'ui'
14import { CodeBlock } from 'ui-patterns/CodeBlock'
15
16import { useAnalyticsBucketWrapperInstance } from '../useAnalyticsBucketWrapperInstance'
17import { getPyicebergSnippet } from './CreateTableInstructions.constants'
18import CommandRender from '@/components/interfaces/Functions/CommandRender'
19import { convertKVStringArrayToJson } from '@/components/interfaces/Integrations/Wrappers/Wrappers.utils'
20import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
21import CopyButton from '@/components/ui/CopyButton'
22import { InlineLink } from '@/components/ui/InlineLink'
23import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
24import {
25 getDecryptedValues,
26 useVaultSecretDecryptedValueQuery,
27} from '@/data/vault/vault-secret-decrypted-value-query'
28import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
29import { DOCS_URL } from '@/lib/constants'
30
31export const CreateTableInstructions = ({
32 hideHeader = false,
33 className,
34}: {
35 hideHeader?: boolean
36 className?: string
37}) => {
38 const { ref, bucketId } = useParams()
39 const { data: project } = useSelectedProjectQuery()
40 const [showKeys, setShowKeys] = useState(false)
41 const [isFetchingSecretsOnCopy, setIsFetchingSecretsOnCopy] = useState(false)
42
43 const { data: projectSettings } = useProjectSettingsV2Query({ projectRef: ref })
44 const { data: wrapperInstance } = useAnalyticsBucketWrapperInstance({ bucketId })
45 const wrapperValues = convertKVStringArrayToJson(wrapperInstance?.server_options ?? [])
46
47 const s3AccessKeyVaultID = wrapperValues.vault_aws_access_key_id
48 const s3SecretKeyVaultID = wrapperValues.vault_aws_secret_access_key
49 const tokenVaultID = wrapperValues.vault_token
50
51 const { data: decryptedS3AccessKey, isPending: isDecryptingS3AccessKey } =
52 useVaultSecretDecryptedValueQuery(
53 {
54 projectRef: project?.ref,
55 connectionString: project?.connectionString,
56 id: s3AccessKeyVaultID,
57 },
58 { enabled: showKeys }
59 )
60
61 const { data: decryptedS3SecretKey, isPending: isDecryptingS3SecretKey } =
62 useVaultSecretDecryptedValueQuery(
63 {
64 projectRef: project?.ref,
65 connectionString: project?.connectionString,
66 id: s3SecretKeyVaultID,
67 },
68 { enabled: showKeys }
69 )
70
71 const { data: decryptedToken, isPending: isDecryptingToken } = useVaultSecretDecryptedValueQuery(
72 {
73 projectRef: project?.ref,
74 connectionString: project?.connectionString,
75 id: tokenVaultID,
76 },
77 { enabled: showKeys }
78 )
79
80 const isFetchingSecretValues =
81 showKeys && (isDecryptingS3AccessKey || isDecryptingS3SecretKey || isDecryptingToken)
82
83 const snippetContent = useMemo(
84 () =>
85 getPyicebergSnippet({
86 ref,
87 warehouse: wrapperValues.warehouse,
88 catalogUri: wrapperValues.catalog_uri,
89 s3Endpoint: wrapperValues['s3.endpoint'],
90 s3Region: projectSettings?.region,
91 s3AccessKey: showKeys ? decryptedS3AccessKey : undefined,
92 s3SecretKey: showKeys ? decryptedS3SecretKey : undefined,
93 token: showKeys ? decryptedToken : undefined,
94 }),
95 [
96 decryptedS3AccessKey,
97 decryptedS3SecretKey,
98 decryptedToken,
99 projectSettings?.region,
100 ref,
101 showKeys,
102 wrapperValues,
103 ]
104 )
105
106 return (
107 <Card className={className}>
108 {!hideHeader && (
109 <CardHeader>
110 <CardTitle>Create your first table via Pyiceberg</CardTitle>
111 </CardHeader>
112 )}
113
114 <Accordion defaultValue={['step-1']} type="multiple">
115 <AccordionItem value="step-1">
116 <AccordionTrigger className="px-6 py-3 text-sm">
117 <div className="flex items-center gap-x-4">
118 <div className="w-6 h-6 rounded-full border flex items-center justify-center text-xs font-mono">
119 1
120 </div>
121 <p className="prose text-sm font-normal">
122 Set up Python project with <code>uv</code>
123 </p>
124 </div>
125 </AccordionTrigger>
126 <AccordionContent className="border-0 px-6 pt-1">
127 <CommandRender
128 commands={[
129 {
130 comment: '1. Install uv as a preferred package manager',
131 command: 'curl -LsSf https://astral.sh/uv/install.sh | sh',
132 jsx: () => 'curl -LsSf https://astral.sh/uv/install.sh | sh',
133 },
134 {
135 comment: '2. Initialize a new Python project with uv',
136 command: 'uv init <project-name>',
137 jsx: () => 'uv init <project-name>',
138 },
139 {
140 comment: '3. Install required packages',
141 command: 'uv add pyiceberg pyarrow pandas',
142 jsx: () => 'uv add pyiceberg pyarrow pandas',
143 },
144 ]}
145 />
146 </AccordionContent>
147 </AccordionItem>
148
149 <AccordionItem value="step-2">
150 <AccordionTrigger className="px-6 py-3 text-sm">
151 <div className="flex items-center gap-x-4">
152 <div className="w-6 h-6 rounded-full border flex items-center justify-center text-xs font-mono">
153 2
154 </div>
155 <p className="prose text-sm font-normal">
156 Replace <code>main.py</code> with the following snippet
157 </p>
158 </div>
159 </AccordionTrigger>
160 <AccordionContent className="border-0 px-6 pt-2">
161 <p className="text-foreground mb-3 prose max-w-full text-sm">
162 The following snippet creates a namespace <code>default</code>, then creates a sample
163 table
164 <code>events</code> into that namespace.
165 </p>
166 <div className="relative group">
167 <CodeBlock
168 hideCopy
169 language="python"
170 className="max-h-[330px]"
171 value={snippetContent}
172 />
173 <div className="flex items-center gap-x-1.5 absolute top-2 right-2">
174 <CopyButton
175 type="default"
176 loading={isFetchingSecretsOnCopy}
177 asyncText={async () => {
178 if (!!decryptedS3AccessKey && !!decryptedS3SecretKey && !!decryptedToken) {
179 return getPyicebergSnippet({
180 ref,
181 warehouse: wrapperValues.warehouse,
182 catalogUri: wrapperValues.catalog_uri,
183 s3Endpoint: wrapperValues['s3.endpoint'],
184 s3Region: projectSettings?.region,
185 s3AccessKey: decryptedS3AccessKey,
186 s3SecretKey: decryptedS3SecretKey,
187 token: decryptedToken,
188 })
189 } else {
190 setIsFetchingSecretsOnCopy(true)
191 const decryptedSecrets = await getDecryptedValues({
192 projectRef: project?.ref,
193 connectionString: project?.connectionString,
194 ids: [s3AccessKeyVaultID, s3SecretKeyVaultID, tokenVaultID],
195 })
196 setIsFetchingSecretsOnCopy(false)
197 return getPyicebergSnippet({
198 ref,
199 warehouse: wrapperValues.warehouse,
200 catalogUri: wrapperValues.catalog_uri,
201 s3Endpoint: wrapperValues['s3.endpoint'],
202 s3Region: projectSettings?.region,
203 s3AccessKey: decryptedSecrets[s3AccessKeyVaultID],
204 s3SecretKey: decryptedSecrets[s3SecretKeyVaultID],
205 token: decryptedSecrets[tokenVaultID],
206 })
207 }
208 }}
209 />
210 <ButtonTooltip
211 type="default"
212 className="w-7"
213 loading={isFetchingSecretValues}
214 onClick={() => setShowKeys(!showKeys)}
215 icon={showKeys ? <EyeOff /> : <Eye />}
216 tooltip={{
217 content: {
218 side: 'bottom',
219 text: showKeys
220 ? 'Hide keys'
221 : isFetchingSecretValues
222 ? 'Retrieving keys'
223 : 'Reveal keys',
224 },
225 }}
226 />
227 </div>
228 </div>
229 </AccordionContent>
230 </AccordionItem>
231
232 <AccordionItem value="step-3">
233 <AccordionTrigger className="px-6 py-3 text-sm">
234 <div className="flex items-center gap-x-4">
235 <div className="w-6 h-6 rounded-full border flex items-center justify-center text-xs font-mono">
236 3
237 </div>
238 <p className="prose text-sm font-normal">Run the Python script</p>
239 </div>
240 </AccordionTrigger>
241 <AccordionContent className="border-0 px-6 pt-2">
242 <CommandRender
243 commands={[
244 {
245 comment: 'Run the main.py script with uv',
246 command: 'uv run main.py',
247 jsx: () => 'uv run main.py',
248 },
249 ]}
250 />
251 </AccordionContent>
252 </AccordionItem>
253 </Accordion>
254
255 <CardFooter className="bg">
256 <p className="text-xs text-foreground-light">
257 Connecting to bucket with other Iceberg clients? Read more in our{' '}
258 <InlineLink href={`${DOCS_URL}/guides/storage/analytics/examples/pyiceberg`}>
259 documentation
260 </InlineLink>
261 .
262 </p>
263 </CardFooter>
264 </Card>
265 )
266}