diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageSettings/S3Connection.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageSettings/S3Connection.tsx new file mode 100644 index 00000000000..1863d0b07c8 --- /dev/null +++ b/apps/studio/components/to-be-cleaned/Storage/StorageSettings/S3Connection.tsx @@ -0,0 +1,343 @@ +import { differenceInDays } from 'date-fns' +import React from 'react' +import toast from 'react-hot-toast' + +import { useParams } from 'common' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import Table from 'components/to-be-cleaned/Table' +import CopyButton from 'components/ui/CopyButton' +import { FormHeader } from 'components/ui/Forms' +import Panel from 'components/ui/Panel' +import { useProjectApiQuery } from 'data/config/project-api-query' +import { useS3AccessKeyCreateMutation } from 'data/storage/s3-access-key-create-mutation' +import { useS3AccessKeyDeleteMutation } from 'data/storage/s3-access-key-delete-mutation' +import { useStorageCredentialsQuery } from 'data/storage/s3-access-key-query' +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + IconMoreVertical, + IconTrash, + cn, +} from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns' +import { Input } from 'ui-patterns/DataInputs/Input' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +export const S3Connection = () => { + const [openCreateCred, setOpenCreateCred] = React.useState(false) + const [showSuccess, setShowSuccess] = React.useState(false) + const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false) + const [deleteCredId, setDeleteCredId] = React.useState(null) + + const { ref: projectRef } = useParams() + const { project, isLoading: projectIsLoading } = useProjectContext() + + const { data: storageCreds, ...storageCredsQuery } = useStorageCredentialsQuery({ + projectRef, + }) + + const hasStorageCreds = storageCreds?.data && storageCreds.data.length > 0 + + const createS3AccessKey = useS3AccessKeyCreateMutation({ + projectRef, + }) + + const deleteS3AccessKey = useS3AccessKeyDeleteMutation({ + projectRef, + }) + + const { data: projectAPI } = useProjectApiQuery({ projectRef: projectRef }) + + function getConnectionURL() { + const projUrl = projectAPI + ? `${projectAPI.autoApiService.protocol}://${projectAPI.autoApiService.endpoint}` + : `https://${projectRef}.supabase.co` + + const url = new URL(projUrl) + url.pathname = '/storage/v1/s3' + return url.toString() + } + + const s3connectionUrl = getConnectionURL() + + return ( + <> +
+ + + + + + {projectIsLoading ? ( + <> + ) : ( + + + + )} + +
+ +
+ { + setOpenCreateCred(open) + if (!open) setShowSuccess(false) + }} + > + + + + + { + if (showSuccess) { + e.preventDefault() + } + }} + > + {showSuccess ? ( + <> + Save your new S3 access keys + + You won't be able to see them again. If you lose these credentials, you'll + need to create a new ones. + + + + + + + + +
+ +
+ + ) : ( + <> + Create new S3 access keys +
{ + e.preventDefault() + + const formData = new FormData(e.target as HTMLFormElement) + const description = formData.get('description') as string + + await createS3AccessKey.mutateAsync({ description }) + setShowSuccess(true) + }} + > + + + + +
+ +
+
+ + )} +
+ + } + /> + +
+ {storageCredsQuery.isLoading ? ( +
+ +
+ ) : ( +
+ Description, + Access key id, + Created at, + , + ]} + body={ + hasStorageCreds ? ( + storageCreds.data?.map((cred: any) => ( + { + setDeleteCredId(cred.id) + setOpenDeleteDialog(true) + }} + /> + )) + ) : ( + + +

No credentials created

+

+ There are no credentials associated with your project yet +

+
+
+ ) + } + /> + + )} + + + + + + Revoke S3 access keys + + This action is irreversible and requests made with these credentials will stop working. + +
+ + +
+
+
+ + ) +} + +function StorageCredItem({ + description, + id, + created_at, + access_key, + onDeleteClick, +}: { + description: string + id: string + created_at: string + access_key: string + onDeleteClick: (id: string) => void +}) { + function daysSince(date: string) { + const now = new Date() + const created = new Date(date) + const diffInDays = differenceInDays(now, created) + + if (diffInDays === 0) { + return 'Today' + } else if (diffInDays === 1) { + return `${diffInDays} day ago` + } else { + return `${diffInDays} days ago` + } + } + + return ( +
+ + + + + + ) +} diff --git a/apps/studio/components/ui/Forms/FormHeader.tsx b/apps/studio/components/ui/Forms/FormHeader.tsx index 0e5c0ad4606..2660123980d 100644 --- a/apps/studio/components/ui/Forms/FormHeader.tsx +++ b/apps/studio/components/ui/Forms/FormHeader.tsx @@ -4,16 +4,19 @@ import ReactMarkdown from 'react-markdown' import { Markdown } from 'components/interfaces/Markdown' import { Button } from 'ui' +import { ReactNode } from 'react' const FormHeader = ({ title, description, docsUrl, + actions, className, }: { title: string description?: string docsUrl?: string + actions?: ReactNode className?: string }) => { return ( @@ -26,13 +29,16 @@ const FormHeader = ({ {description && } - {docsUrl !== undefined && ( - - )} +
+ {docsUrl !== undefined && ( + + )} + {actions} +
) } diff --git a/apps/studio/data/api.d.ts b/apps/studio/data/api.d.ts index ee2eea32c94..95dbba5ee6a 100644 --- a/apps/studio/data/api.d.ts +++ b/apps/studio/data/api.d.ts @@ -45,6 +45,10 @@ export interface paths { /** Send upgrade survey to survey_responses table */ post: operations['SendUpgradeSurveyController_sendUpgradeSurvey'] } + '/platform/feedback/docs': { + /** Send feedback on docs */ + post: operations['SendDocsFeedbackController_sendDocsFeedback'] + } '/platform/signup': { /** Sign up with email and password */ post: operations['SignUpController_signUp'] @@ -591,6 +595,50 @@ export interface paths { /** Gets project's usage api requests count */ get: operations['UsageApiController_getApiRequestsCount'] } + '/platform/projects/{ref}/analytics/warehouse/tenant': { + /** Gets project's warehouse tenant from logflare */ + get: operations['TenantController_getTenant'] + } + '/platform/projects/{ref}/analytics/warehouse/collections': { + /** Lists project's warehouse collections from logflare */ + get: operations['CollectionController_listCollections'] + /** Create a warehouse collection */ + post: operations['CollectionController_createCollection'] + } + '/platform/projects/{ref}/analytics/warehouse/collections/{token}': { + /** Get a warehouse collection */ + get: operations['CollectionController_getCollection'] + /** Delete a warehouse collection */ + delete: operations['CollectionController_deleteCollection'] + /** Update a warehouse collection */ + patch: operations['CollectionController_updateCollection'] + } + '/platform/projects/{ref}/analytics/warehouse/access-tokens': { + /** Lists project's warehouse access tokens from logflare */ + get: operations['AccessTokenController_listAccessTokens'] + /** Create a warehouse access token */ + post: operations['AccessTokenController_createAccessToken'] + } + '/platform/projects/{ref}/analytics/warehouse/access-tokens/{token}': { + /** Delete a warehouse access token */ + delete: operations['AccessTokenController_deleteAccessToken'] + } + '/platform/projects/{ref}/analytics/warehouse/endpoints': { + /** Lists project's warehouse endpoints from logflare */ + get: operations['EndpointController_listEndpoints'] + /** Create a warehouse endpoint */ + post: operations['EndpointController_createEndpoint'] + } + '/platform/projects/{ref}/analytics/warehouse/endpoints/{token}': { + /** Update a warehouse endpoint */ + put: operations['EndpointController_updateEndpoint'] + /** Delete a warehouse endpoint */ + delete: operations['EndpointController_deleteEndpoint'] + } + '/platform/projects/{ref}/analytics/warehouse/query': { + /** Lists project's warehouse queries from logflare */ + get: operations['QueryController_runQuery'] + } '/platform/projects/{ref}/config/pgbouncer': { /** Gets project's pgbouncer config */ get: operations['PgbouncerConfigController_getPgbouncerConfig'] @@ -707,6 +755,16 @@ export interface paths { /** Deletes objects */ delete: operations['StorageObjectsController_deleteObjects'] } + '/platform/storage/{ref}/credentials': { + /** Gets project storage credentials */ + get: operations['StorageS3CredentialsController_getAllCredentials'] + /** Creates project storage credential */ + post: operations['StorageS3CredentialsController_createCredential'] + } + '/platform/storage/{ref}/credentials/{id}': { + /** Deletes project storage credential */ + delete: operations['StorageS3CredentialsController_deleteCredential'] + } '/platform/stripe/invoices': { /** Gets invoices for the given customer */ get: operations['InvoicesController_getInvoices'] @@ -864,12 +922,24 @@ export interface paths { /** Reset JWT if leaked keys found by GitHub secret scanning */ post: operations['GithubSecretAlertController_resetJwt'] } + '/system/projects/{ref}/functions/{function_slug}': { + /** + * Update a function + * @description Updates a function with the specified slug and project. + */ + patch: operations['SystemFunctionSlugController_updateFunction'] + } '/system/projects/{ref}/functions': { /** * List all functions * @description Returns all functions you've previously added to the specified project. */ get: operations['SystemFunctionsController_getFunctions'] + /** + * Create a function + * @description Creates a function and adds it to the specified project. + */ + post: operations['SystemFunctionsController_createFunction'] /** Deletes all Edge Functions from a project */ delete: operations['SystemFunctionsController_systemDeleteAllFunctions'] } @@ -944,6 +1014,10 @@ export interface paths { /** Updates restriction status of an org */ put: operations['OrgRestrictionsSystemController_updateRestriction'] } + '/system/partner-organizations': { + /** Creates a partner organization */ + post: operations['AwsPartnerOrganizationsSystemController_createPartnerOrganization'] + } '/system/integrations/vercel/webhooks': { /** Processes Vercel event */ post: operations['VercelWebhooksController_processEvent'] @@ -1357,6 +1431,50 @@ export interface paths { /** Gets project's usage api requests count */ get: operations['UsageApiController_getApiRequestsCount'] } + '/v0/projects/{ref}/analytics/warehouse/tenant': { + /** Gets project's warehouse tenant from logflare */ + get: operations['TenantController_getTenant'] + } + '/v0/projects/{ref}/analytics/warehouse/collections': { + /** Lists project's warehouse collections from logflare */ + get: operations['CollectionController_listCollections'] + /** Create a warehouse collection */ + post: operations['CollectionController_createCollection'] + } + '/v0/projects/{ref}/analytics/warehouse/collections/{token}': { + /** Get a warehouse collection */ + get: operations['CollectionController_getCollection'] + /** Delete a warehouse collection */ + delete: operations['CollectionController_deleteCollection'] + /** Update a warehouse collection */ + patch: operations['CollectionController_updateCollection'] + } + '/v0/projects/{ref}/analytics/warehouse/access-tokens': { + /** Lists project's warehouse access tokens from logflare */ + get: operations['AccessTokenController_listAccessTokens'] + /** Create a warehouse access token */ + post: operations['AccessTokenController_createAccessToken'] + } + '/v0/projects/{ref}/analytics/warehouse/access-tokens/{token}': { + /** Delete a warehouse access token */ + delete: operations['AccessTokenController_deleteAccessToken'] + } + '/v0/projects/{ref}/analytics/warehouse/endpoints': { + /** Lists project's warehouse endpoints from logflare */ + get: operations['EndpointController_listEndpoints'] + /** Create a warehouse endpoint */ + post: operations['EndpointController_createEndpoint'] + } + '/v0/projects/{ref}/analytics/warehouse/endpoints/{token}': { + /** Update a warehouse endpoint */ + put: operations['EndpointController_updateEndpoint'] + /** Delete a warehouse endpoint */ + delete: operations['EndpointController_deleteEndpoint'] + } + '/v0/projects/{ref}/analytics/warehouse/query': { + /** Lists project's warehouse queries from logflare */ + get: operations['QueryController_runQuery'] + } '/v0/projects/{ref}/config/pgbouncer': { /** Gets project's pgbouncer config */ get: operations['PgbouncerConfigController_getPgbouncerConfig'] @@ -1455,6 +1573,16 @@ export interface paths { /** Deletes objects */ delete: operations['StorageObjectsController_deleteObjects'] } + '/v0/storage/{ref}/credentials': { + /** Gets project storage credentials */ + get: operations['StorageS3CredentialsController_getAllCredentials'] + /** Creates project storage credential */ + post: operations['StorageS3CredentialsController_createCredential'] + } + '/v0/storage/{ref}/credentials/{id}': { + /** Deletes project storage credential */ + delete: operations['StorageS3CredentialsController_deleteCredential'] + } '/v1/branches/{branch_id}': { /** * Get database branch config @@ -1651,6 +1779,18 @@ export interface paths { /** Updates a project's auth config */ patch: operations['V1AuthConfigController_updateV1AuthConfig'] } + '/v1/projects/{ref}/config/auth/third-party-auth': { + /** Lists all third-party auth integrations */ + get: operations['ThirdPartyAuthController_listTPAForProject'] + /** Creates a new third-party auth integration */ + post: operations['ThirdPartyAuthController_createTPAForProject'] + } + '/v1/projects/{ref}/config/auth/third-party-auth/{tpa_id}': { + /** Get a third-party integration */ + get: operations['ThirdPartyAuthController_getTPAForProject'] + /** Removes a third-party auth integration */ + delete: operations['ThirdPartyAuthController_deleteTPAForProject'] + } '/v1/projects/{ref}/config/auth/sso/providers': { /** Lists all SSO providers */ get: operations['ProvidersController_listAllProviders'] @@ -1878,6 +2018,12 @@ export interface components { reasons: string[] additionalFeedback?: string } + SendDocsFeedbackBody: { + page: string + isHelpful: boolean + title: string + feedback: string + } SignUpBody: { email: string password: string @@ -1917,11 +2063,13 @@ export interface components { MAILER_SUBJECTS_RECOVERY: string MAILER_SUBJECTS_EMAIL_CHANGE: string MAILER_SUBJECTS_MAGIC_LINK: string + MAILER_SUBJECTS_REAUTHENTICATION: string MAILER_TEMPLATES_INVITE_CONTENT: string MAILER_TEMPLATES_CONFIRMATION_CONTENT: string MAILER_TEMPLATES_RECOVERY_CONTENT: string MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT: string MAILER_TEMPLATES_MAGIC_LINK_CONTENT: string + MAILER_TEMPLATES_REAUTHENTICATION_CONTENT: string MFA_MAX_ENROLLED_FACTORS: number URI_ALLOW_LIST: string EXTERNAL_ANONYMOUS_USERS_ENABLED: boolean @@ -1977,6 +2125,10 @@ export interface components { HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI: string HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: boolean HOOK_CUSTOM_ACCESS_TOKEN_URI: string + HOOK_SEND_SMS_ENABLED: boolean + HOOK_SEND_SMS_URI: string + HOOK_SEND_EMAIL_ENABLED: boolean + HOOK_SEND_EMAIL_URI: string EXTERNAL_APPLE_ENABLED: boolean EXTERNAL_APPLE_CLIENT_ID: string EXTERNAL_APPLE_SECRET: string @@ -2060,7 +2212,9 @@ export interface components { MAILER_SUBJECTS_RECOVERY?: string MAILER_SUBJECTS_EMAIL_CHANGE?: string MAILER_SUBJECTS_MAGIC_LINK?: string + MAILER_SUBJECTS_REAUTHENTICATION?: string MAILER_TEMPLATES_INVITE_CONTENT?: string + MAILER_TEMPLATES_REAUTHENTICATION_CONTENT?: string MAILER_TEMPLATES_CONFIRMATION_CONTENT?: string MAILER_TEMPLATES_RECOVERY_CONTENT?: string MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT?: string @@ -2125,6 +2279,10 @@ export interface components { HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI?: string HOOK_CUSTOM_ACCESS_TOKEN_ENABLED?: boolean HOOK_CUSTOM_ACCESS_TOKEN_URI?: string + HOOK_SEND_SMS_ENABLED?: boolean + HOOK_SEND_SMS_URI?: string + HOOK_SEND_EMAIL_ENABLED?: boolean + HOOK_SEND_EMAIL_URI?: string EXTERNAL_APPLE_ENABLED?: boolean EXTERNAL_APPLE_CLIENT_ID?: string EXTERNAL_APPLE_SECRET?: string @@ -2810,7 +2968,7 @@ export interface components { payment_method_card_details?: components['schemas']['PaymentMethodCardDetails'] billing_via_partner: boolean /** @enum {string} */ - billing_partner: 'fly' + billing_partner: 'fly' | 'aws' scheduled_plan_change: components['schemas']['ScheduledPlanChange'] | null customer_balance: number nano_enabled: boolean @@ -3807,6 +3965,30 @@ export interface components { > result?: Record[] } + LFUser: { + token: string + metadata: { + project_ref?: string + } + } + LFSource: { + token: string + id: number + name: string + } + LFEndpoint: { + token: string + id: number + name: string + description: string + query: string + language: Record + sandboxable: boolean | null + cache_duration_seconds: number + proactive_requerying_seconds: number + max_limit: number + enable_auth: number + } PgbouncerConfigResponse: { default_pool_size?: number ignore_startup_parameters?: string @@ -4140,6 +4322,23 @@ export interface components { DeleteObjectsBody: { paths: string[] } + GetStorageCredential: { + id: string + description: string + created_at: string + } + GetStorageCredentialsResponse: { + data: components['schemas']['GetStorageCredential'][] + } + CreateStorageCredentialBody: { + description: string + } + CreateStorageCredentialResponse: { + id: string + access_key: string + secret_key: string + description: string + } Invoice: { id: string invoice_pdf: string @@ -4377,6 +4576,11 @@ export interface components { public_key: string token_name?: string } + V1UpdateFunctionBody: { + name?: string + body?: string + verify_jwt?: boolean + } FunctionResponse: { id: string slug: string @@ -4391,6 +4595,12 @@ export interface components { entrypoint_path?: string import_map_path?: string } + V1CreateFunctionBody: { + slug: string + name: string + body: string + verify_jwt?: boolean + } SecretResponse: { name: string value: string @@ -4545,6 +4755,15 @@ export interface components { restriction_data?: components['schemas']['RestrictionData'] message?: string } + CreateAwsPartnerOrganizationBody: { + primary_email: string + name: string + } + AwsPartnerOrganizationResponse: { + id: number + slug: string + name: string + } GetMetricsBody: { /** @enum {string} */ metric: 'user_queries' @@ -4634,6 +4853,19 @@ export interface components { */ created_at: string database?: components['schemas']['V1DatabaseResponse'] + /** @enum {string} */ + status: + | 'ACTIVE_HEALTHY' + | 'ACTIVE_UNHEALTHY' + | 'COMING_UP' + | 'GOING_DOWN' + | 'INACTIVE' + | 'INIT_FAILED' + | 'REMOVED' + | 'RESTORING' + | 'UNKNOWN' + | 'UPGRADING' + | 'PAUSING' } V1CreateProjectBody: { /** @description Database password */ @@ -4673,6 +4905,11 @@ export interface components { /** @deprecated */ kps_enabled?: boolean desired_instance_size?: components['schemas']['DesiredInstanceSize'] + /** + * @description Template URL used to create the project from the CLI. + * @example https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone + */ + template_url?: string } ApiKeyResponse: { name: string @@ -4955,11 +5192,13 @@ export interface components { mailer_subjects_invite: string | null mailer_subjects_magic_link: string | null mailer_subjects_recovery: string | null + mailer_subjects_reauthentication: string | null mailer_templates_confirmation_content: string | null mailer_templates_email_change_content: string | null mailer_templates_invite_content: string | null mailer_templates_magic_link_content: string | null mailer_templates_recovery_content: string | null + mailer_templates_reauthentication_content: string | null mfa_max_enrolled_factors: number | null password_hibp_enabled: boolean | null password_min_length: number | null @@ -5031,11 +5270,13 @@ export interface components { mailer_subjects_recovery?: string mailer_subjects_email_change?: string mailer_subjects_magic_link?: string + mailer_subjects_reauthentication?: string mailer_templates_invite_content?: string mailer_templates_confirmation_content?: string mailer_templates_recovery_content?: string mailer_templates_email_change_content?: string mailer_templates_magic_link_content?: string + mailer_templates_reauthentication_content?: string mfa_max_enrolled_factors?: number uri_allow_list?: string external_anonymous_users_enabled?: boolean @@ -5096,6 +5337,10 @@ export interface components { hook_password_verification_attempt_uri?: string hook_custom_access_token_enabled?: boolean hook_custom_access_token_uri?: string + hook_send_sms_enabled?: boolean + hook_send_sms_uri?: string + hook_send_email_enabled?: boolean + hook_send_email_uri?: string external_apple_enabled?: boolean external_apple_client_id?: string external_apple_secret?: string @@ -5161,6 +5406,22 @@ export interface components { external_zoom_client_id?: string external_zoom_secret?: string } + CreateThirdPartyAuthBody: { + oidc_issuer_url?: string + jwks_url?: string + custom_jwks?: Record + } + ThirdPartyAuth: { + id: string + type: string + oidc_issuer_url?: string | null + jwks_url?: string | null + custom_jwks?: unknown + resolved_jwks?: unknown + inserted_at: string + updated_at: string + resolved_at?: string | null + } AttributeValue: { default?: Record | number | string | boolean name?: string @@ -5262,12 +5523,6 @@ export interface components { V1RestorePitrBody: { recovery_time_target_unix: number } - V1CreateFunctionBody: { - slug: string - name: string - body: string - verify_jwt?: boolean - } FunctionSlugResponse: { id: string slug: string @@ -5282,11 +5537,6 @@ export interface components { entrypoint_path?: string import_map_path?: string } - V1UpdateFunctionBody: { - name?: string - body?: string - verify_jwt?: boolean - } V1StorageBucketResponse: { id: string name: string @@ -5699,6 +5949,25 @@ export interface operations { } } } + /** Send feedback on docs */ + SendDocsFeedbackController_sendDocsFeedback: { + requestBody: { + content: { + 'application/json': components['schemas']['SendDocsFeedbackBody'] + } + } + responses: { + 201: { + content: { + 'application/json': components['schemas']['SendFeedbackResponse'] + } + } + /** @description Failed to send feedback for docs */ + 500: { + content: never + } + } + } /** Sign up with email and password */ SignUpController_signUp: { requestBody: { @@ -5919,7 +6188,13 @@ export interface operations { parameters: { path: { ref: string - template: 'confirmation' | 'email-change' | 'invite' | 'magic-link' | 'recovery' + template: + | 'confirmation' + | 'email-change' + | 'invite' + | 'magic-link' + | 'recovery' + | 'reauthentication' } } responses: { @@ -7909,32 +8184,22 @@ export interface operations { } } } - /** Run sql query */ + /** Lists project's warehouse queries from logflare */ QueryController_runQuery: { parameters: { - header: { - 'x-connection-encrypted': string - } path: { /** @description Project ref */ ref: string } } - requestBody: { - content: { - 'application/json': components['schemas']['RunQueryBody'] - } - } responses: { - 201: { - content: { - 'application/json': Record - } + 200: { + content: never } 403: { content: never } - /** @description Failed to run sql query */ + /** @description Failed to fetch warehouse endpoints */ 500: { content: never } @@ -9720,6 +9985,249 @@ export interface operations { } } } + /** Gets project's warehouse tenant from logflare */ + TenantController_getTenant: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['LFUser'] + } + } + 403: { + content: never + } + /** @description Failed to fetch or provision warehouse tenant */ + 500: { + content: never + } + } + } + /** Lists project's warehouse collections from logflare */ + CollectionController_listCollections: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['LFSource'][] + } + } + 403: { + content: never + } + /** @description Failed to fetch warehouse collections */ + 500: { + content: never + } + } + } + /** Create a warehouse collection */ + CollectionController_createCollection: { + responses: { + 201: { + content: { + 'application/json': components['schemas']['LFSource'] + } + } + 403: { + content: never + } + /** @description Failed to create warehouse collection */ + 500: { + content: never + } + } + } + /** Get a warehouse collection */ + CollectionController_getCollection: { + responses: { + 200: { + content: { + 'application/json': components['schemas']['LFSource'] + } + } + 403: { + content: never + } + /** @description Failed to fetch warehouse collection */ + 500: { + content: never + } + } + } + /** Delete a warehouse collection */ + CollectionController_deleteCollection: { + responses: { + 200: { + content: { + 'application/json': components['schemas']['LFSource'] + } + } + 403: { + content: never + } + /** @description Failed to delete warehouse collection */ + 500: { + content: never + } + } + } + /** Update a warehouse collection */ + CollectionController_updateCollection: { + responses: { + 200: { + content: { + 'application/json': components['schemas']['LFSource'] + } + } + 403: { + content: never + } + /** @description Failed to update warehouse collection */ + 500: { + content: never + } + } + } + /** Lists project's warehouse access tokens from logflare */ + AccessTokenController_listAccessTokens: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['LFSource'][] + } + } + 403: { + content: never + } + /** @description Failed to fetch warehouse access tokens */ + 500: { + content: never + } + } + } + /** Create a warehouse access token */ + AccessTokenController_createAccessToken: { + responses: { + 201: { + content: { + 'application/json': components['schemas']['LFSource'] + } + } + 403: { + content: never + } + /** @description Failed to create warehouse access token */ + 500: { + content: never + } + } + } + /** Delete a warehouse access token */ + AccessTokenController_deleteAccessToken: { + responses: { + 200: { + content: { + 'application/json': components['schemas']['LFSource'] + } + } + 403: { + content: never + } + /** @description Failed to delete warehouse access token */ + 500: { + content: never + } + } + } + /** Lists project's warehouse endpoints from logflare */ + EndpointController_listEndpoints: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['LFEndpoint'][] + } + } + 403: { + content: never + } + /** @description Failed to fetch warehouse endpoints */ + 500: { + content: never + } + } + } + /** Create a warehouse endpoint */ + EndpointController_createEndpoint: { + responses: { + 201: { + content: { + 'application/json': components['schemas']['LFEndpoint'] + } + } + 403: { + content: never + } + /** @description Failed to create warehouse endpoint */ + 500: { + content: never + } + } + } + /** Update a warehouse endpoint */ + EndpointController_updateEndpoint: { + responses: { + 200: { + content: { + 'application/json': components['schemas']['LFEndpoint'] + } + } + 403: { + content: never + } + /** @description Failed to update warehouse endpoint */ + 500: { + content: never + } + } + } + /** Delete a warehouse endpoint */ + EndpointController_deleteEndpoint: { + responses: { + 200: { + content: never + } + 403: { + content: never + } + /** @description Failed to delete warehouse endpoint */ + 500: { + content: never + } + } + } /** Gets project's pgbouncer config */ PgbouncerConfigController_getPgbouncerConfig: { parameters: { @@ -10506,6 +11014,80 @@ export interface operations { } } } + /** Gets project storage credentials */ + StorageS3CredentialsController_getAllCredentials: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['GetStorageCredentialsResponse'] + } + } + 403: { + content: never + } + /** @description Failed to get project storage credentials */ + 500: { + content: never + } + } + } + /** Creates project storage credential */ + StorageS3CredentialsController_createCredential: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['CreateStorageCredentialBody'] + } + } + responses: { + 201: { + content: { + 'application/json': components['schemas']['CreateStorageCredentialResponse'] + } + } + 403: { + content: never + } + /** @description Failed to create project storage credential */ + 500: { + content: never + } + } + } + /** Deletes project storage credential */ + StorageS3CredentialsController_deleteCredential: { + parameters: { + path: { + /** @description Project ref */ + ref: string + /** @description Storage credential id */ + id: string + } + } + responses: { + 200: { + content: never + } + 403: { + content: never + } + /** @description Failed to delete project storage credential */ + 500: { + content: never + } + } + } /** Gets invoices for the given customer */ InvoicesController_getInvoices: { parameters: { @@ -11197,7 +11779,13 @@ export interface operations { parameters: { path: { ref: string - template: 'confirmation' | 'email-change' | 'invite' | 'magic-link' | 'recovery' + template: + | 'confirmation' + | 'email-change' + | 'invite' + | 'magic-link' + | 'recovery' + | 'reauthentication' } } responses: { @@ -11359,6 +11947,48 @@ export interface operations { } } } + /** + * Update a function + * @description Updates a function with the specified slug and project. + */ + SystemFunctionSlugController_updateFunction: { + parameters: { + query?: { + slug?: string + name?: string + verify_jwt?: boolean + import_map?: boolean + entrypoint_path?: string + import_map_path?: string + } + path: { + /** @description Project ref */ + ref: string + /** @description Function slug */ + function_slug: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['V1UpdateFunctionBody'] + 'application/vnd.denoland.eszip': components['schemas']['V1UpdateFunctionBody'] + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['FunctionResponse'] + } + } + 403: { + content: never + } + /** @description Failed to update function with given slug */ + 500: { + content: never + } + } + } /** * List all functions * @description Returns all functions you've previously added to the specified project. @@ -11385,6 +12015,46 @@ export interface operations { } } } + /** + * Create a function + * @description Creates a function and adds it to the specified project. + */ + SystemFunctionsController_createFunction: { + parameters: { + query?: { + slug?: string + name?: string + verify_jwt?: boolean + import_map?: boolean + entrypoint_path?: string + import_map_path?: string + } + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['V1CreateFunctionBody'] + 'application/vnd.denoland.eszip': components['schemas']['V1CreateFunctionBody'] + } + } + responses: { + 201: { + content: { + 'application/json': components['schemas']['FunctionResponse'] + } + } + 403: { + content: never + } + /** @description Failed to create project's function */ + 500: { + content: never + } + } + } /** Deletes all Edge Functions from a project */ SystemFunctionsController_systemDeleteAllFunctions: { parameters: { @@ -11753,6 +12423,25 @@ export interface operations { } } } + /** Creates a partner organization */ + AwsPartnerOrganizationsSystemController_createPartnerOrganization: { + requestBody: { + content: { + 'application/json': components['schemas']['CreateAwsPartnerOrganizationBody'] + } + } + responses: { + 201: { + content: { + 'application/json': components['schemas']['AwsPartnerOrganizationResponse'] + } + } + /** @description Unexpected error creating a partner organization */ + 500: { + content: never + } + } + } /** Processes Vercel event */ VercelWebhooksController_processEvent: { parameters: { @@ -12879,6 +13568,89 @@ export interface operations { } } } + /** Lists all third-party auth integrations */ + ThirdPartyAuthController_listTPAForProject: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['ThirdPartyAuth'][] + } + } + 403: { + content: never + } + } + } + /** Creates a new third-party auth integration */ + ThirdPartyAuthController_createTPAForProject: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['CreateThirdPartyAuthBody'] + } + } + responses: { + 201: { + content: { + 'application/json': components['schemas']['ThirdPartyAuth'] + } + } + 403: { + content: never + } + } + } + /** Get a third-party integration */ + ThirdPartyAuthController_getTPAForProject: { + parameters: { + path: { + /** @description Project ref */ + ref: string + tpa_id: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['ThirdPartyAuth'] + } + } + 403: { + content: never + } + } + } + /** Removes a third-party auth integration */ + ThirdPartyAuthController_deleteTPAForProject: { + parameters: { + path: { + /** @description Project ref */ + ref: string + tpa_id: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['ThirdPartyAuth'] + } + } + 403: { + content: never + } + } + } /** Lists all SSO providers */ ProvidersController_listAllProviders: { parameters: { diff --git a/apps/studio/data/sql/execute-sql-query.ts b/apps/studio/data/sql/execute-sql-query.ts index 5e8b4fbf8a1..b3aec849476 100644 --- a/apps/studio/data/sql/execute-sql-query.ts +++ b/apps/studio/data/sql/execute-sql-query.ts @@ -52,7 +52,7 @@ export async function executeSql( }, body: { query: sql }, headers: Object.fromEntries(headers), - }) + } as any) // Needed to fix generated api types for now if (error) { if ( diff --git a/apps/studio/data/storage/s3-access-key-create-mutation.ts b/apps/studio/data/storage/s3-access-key-create-mutation.ts new file mode 100644 index 00000000000..c97b186d714 --- /dev/null +++ b/apps/studio/data/storage/s3-access-key-create-mutation.ts @@ -0,0 +1,47 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { storageCredentialsKeys } from './s3-access-key-keys' +import { post } from 'data/fetchers' + +type CreateS3AccessKeyCredential = { + description: string + projectRef?: string +} +const createS3AccessKeyCredential = async ({ + description, + projectRef, +}: CreateS3AccessKeyCredential) => { + if (!projectRef) { + throw new Error('projectRef is required') + } + + const res = await post('/platform/storage/{ref}/credentials', { + params: { + path: { + ref: projectRef, + }, + }, + body: { + description, + }, + }) + + return res +} + +type S3AccessKeyCreateMutation = { + projectRef?: string +} + +export function useS3AccessKeyCreateMutation({ projectRef }: S3AccessKeyCreateMutation) { + const queryClient = useQueryClient() + + const keys = storageCredentialsKeys.credentials(projectRef) + + return useMutation({ + mutationFn: ({ description }: { description: string }) => + createS3AccessKeyCredential({ description, projectRef }), + onSettled: () => { + queryClient.invalidateQueries(keys) + }, + }) +} diff --git a/apps/studio/data/storage/s3-access-key-delete-mutation.ts b/apps/studio/data/storage/s3-access-key-delete-mutation.ts new file mode 100644 index 00000000000..06fef9a3717 --- /dev/null +++ b/apps/studio/data/storage/s3-access-key-delete-mutation.ts @@ -0,0 +1,38 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { storageCredentialsKeys } from './s3-access-key-keys' +import { del } from 'data/fetchers' + +type S3AccessKeyDeleteMutation = { + projectRef?: string + id?: string +} + +const deleteS3AccessKeyCredential = async ({ projectRef, id }: S3AccessKeyDeleteMutation) => { + if (!projectRef || !id) { + throw new Error('projectRef and id are required') + } + + const res = await del('/platform/storage/{ref}/credentials/{id}', { + params: { + path: { + ref: projectRef, + id, + }, + }, + }) + + return res +} + +export function useS3AccessKeyDeleteMutation({ projectRef }: S3AccessKeyDeleteMutation) { + const queryClient = useQueryClient() + + const keys = storageCredentialsKeys.credentials(projectRef) + + return useMutation({ + mutationFn: ({ id }: { id: string }) => deleteS3AccessKeyCredential({ projectRef, id }), + onSettled: () => { + queryClient.invalidateQueries(keys) + }, + }) +} diff --git a/apps/studio/data/storage/s3-access-key-keys.ts b/apps/studio/data/storage/s3-access-key-keys.ts new file mode 100644 index 00000000000..cdaf774b7c2 --- /dev/null +++ b/apps/studio/data/storage/s3-access-key-keys.ts @@ -0,0 +1,4 @@ +export const storageCredentialsKeys = { + credentials: (projectRef: string | undefined) => + ['projects', projectRef, 'storage-credentials'] as const, +} diff --git a/apps/studio/data/storage/s3-access-key-query.ts b/apps/studio/data/storage/s3-access-key-query.ts new file mode 100644 index 00000000000..c53c09f6271 --- /dev/null +++ b/apps/studio/data/storage/s3-access-key-query.ts @@ -0,0 +1,37 @@ +import { useQuery } from '@tanstack/react-query' +import { storageCredentialsKeys } from './s3-access-key-keys' +import { get } from 'data/fetchers' + +type FetchStorageCredentials = { + projectRef?: string +} +async function fetchStorageCredentials({ projectRef }: FetchStorageCredentials) { + if (!projectRef) { + throw new Error('projectRef is required') + } + + const res = await get('/platform/storage/{ref}/credentials', { + params: { + path: { + ref: projectRef, + }, + }, + }) + + return res.data +} + +type StorageCredentialsQuery = { + projectRef?: string +} +export function useStorageCredentialsQuery({ projectRef }: StorageCredentialsQuery) { + const keys = storageCredentialsKeys.credentials(projectRef) + + const query = useQuery({ + queryKey: keys, + queryFn: () => fetchStorageCredentials({ projectRef }), + enabled: projectRef !== undefined, + }) + + return query +} diff --git a/apps/studio/pages/project/[ref]/settings/storage.tsx b/apps/studio/pages/project/[ref]/settings/storage.tsx index ecbcb6b4fdc..8895ab41832 100644 --- a/apps/studio/pages/project/[ref]/settings/storage.tsx +++ b/apps/studio/pages/project/[ref]/settings/storage.tsx @@ -1,11 +1,16 @@ import { SettingsLayout } from 'components/layouts' import { StorageSettings } from 'components/to-be-cleaned/Storage' +import { S3Connection } from 'components/to-be-cleaned/Storage/StorageSettings/S3Connection' +import { useFlag } from 'hooks' import type { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => { + const showS3Connection = useFlag('showS3Connection') + return ( -
+
+ {showS3Connection && }
) } diff --git a/packages/ui/src/components/Button/Button.tsx b/packages/ui/src/components/Button/Button.tsx index a0d36818e1a..e5936178482 100644 --- a/packages/ui/src/components/Button/Button.tsx +++ b/packages/ui/src/components/Button/Button.tsx @@ -218,12 +218,12 @@ const Button = forwardRef( ref ) => { const Comp = asChild ? Slot : 'button' - const { className, disabled } = props + const { className } = props const showIcon = loading || icon // decrecating 'showIcon' for rightIcon const _iconLeft: React.ReactNode = icon ?? iconLeft // if loading, button is disabled - props.disabled = loading ? true : props.disabled + const disabled = loading === true || props.disabled return ( ( data-size={size} type={htmlType} {...props} + disabled={disabled} className={cn(buttonVariants({ type, size, disabled, block, rounded }), className)} > {asChild ? (
{description} +
+ {access_key} + + + +
+
{daysSince(created_at)} + + + + + + { + e.preventDefault() + onDeleteClick(id) + }} + > + + Revoke credentials + + + +