From 5ec37eb3de295f741a91a5a5031b2fcce87971dc Mon Sep 17 00:00:00 2001 From: Raminder Singh Date: Thu, 17 Oct 2024 19:50:28 +0530 Subject: [PATCH] create publication wip --- .../Replication/CreatePublicationModal.tsx | 120 ++++++++++++++++++ .../interfaces/Replication/Publications.tsx | 23 ++-- .../create-publication-mutation.ts | 66 ++++++++++ .../replication/create-source-mutation.ts | 12 -- apps/studio/data/replication/keys.ts | 2 + apps/studio/data/replication/tables-query.ts | 36 ++++++ packages/api-types/types/api.d.ts | 93 +++++++++++++- 7 files changed, 327 insertions(+), 25 deletions(-) create mode 100644 apps/studio/components/interfaces/Replication/CreatePublicationModal.tsx create mode 100644 apps/studio/data/replication/create-publication-mutation.ts create mode 100644 apps/studio/data/replication/tables-query.ts diff --git a/apps/studio/components/interfaces/Replication/CreatePublicationModal.tsx b/apps/studio/components/interfaces/Replication/CreatePublicationModal.tsx new file mode 100644 index 00000000000..c76b3beeda2 --- /dev/null +++ b/apps/studio/components/interfaces/Replication/CreatePublicationModal.tsx @@ -0,0 +1,120 @@ +import { useParams } from 'common' +import { useCreatePublicationMutation } from 'data/replication/create-publication-mutation' +import { useReplicationTablesQuery } from 'data/replication/tables-query' +import { toast } from 'sonner' +import { Modal, Form, Input, Checkbox, Button } from 'ui' + +export interface CreatePublicationModalProps { + visible: boolean + sourceId: number + onClose: () => void +} + +const CreatePublicationModal = ({ visible, onClose, sourceId }: CreatePublicationModalProps) => { + const { ref } = useParams() + const { data: tables } = useReplicationTablesQuery({ + projectRef: ref, + sourceId, + }) + const { mutate: createPublication, isLoading: isCreating } = useCreatePublicationMutation({ + onSuccess: (res) => { + toast.success('Successfully created publication') + onClose() + }, + }) + const validate = (values: any) => { + const errors = {} as any + if (!values.publication_name) { + errors.publication_name = 'Please provide a name for your publication' + } + return errors + } + const onSubmit = (values: any) => { + let requested_tables = [] + if (tables) { + for (const table in tables) { + const key = tables[table] + const schema = values[key.schema] + if (schema) { + let lowercase_key = key.name.toLowerCase() + const name = schema[lowercase_key] + if (name) { + if (name.length !== 0) { + requested_tables.push({ schema: key.schema, name: key.name }) + } + } + } + } + createPublication({ + projectRef: ref!, + sourceId, + name: values.publication_name, + tables: requested_tables, + }) + } + } + const initialValues = { + publication_name: '', + } + + return ( + onClose()} + > +
+ {({ values }: { values: any }) => { + return ( + <> + + + + +
Publication tables
+ {tables?.length === 0 ? ( +
No tables in database
+ ) : ( + tables?.map((table) => { + let tableLabel = `${table.schema}.${table.name}` + let tableName = `${table.schema}/${table.name}` + return ( +
+ +
+ ) + }) + )} +
+ + + + + + + ) + }} + +
+ ) +} + +export default CreatePublicationModal diff --git a/apps/studio/components/interfaces/Replication/Publications.tsx b/apps/studio/components/interfaces/Replication/Publications.tsx index 69a57160465..4fc17c20fc1 100644 --- a/apps/studio/components/interfaces/Replication/Publications.tsx +++ b/apps/studio/components/interfaces/Replication/Publications.tsx @@ -7,34 +7,37 @@ import { useReplicationSourcesQuery } from 'data/replication/sources-query' import { useParams } from 'common' import { useReplicationPublicationsQuery } from 'data/replication/publications-query' import { Button } from 'ui' +import { useState } from 'react' +import CreatePublicationModal from './CreatePublicationModal' export const ReplicationPublications = () => { const { ref } = useParams() const { data } = useReplicationSourcesQuery({ projectRef: ref, }) + const [showCreatePublicationModal, setShowCreatePublicationModal] = useState(false) const sources = data ?? [] - let publication_source_id = null + let source_id = null for (let i = 0; i < sources.length; i++) { const source = sources[i] if (source.config.Postgres!.host.startsWith(`db.${ref}`)) { - if (publication_source_id === null) { - publication_source_id = source.id + if (source_id === null) { + source_id = source.id } else { throw new Error(`multiple sources for ref ${ref}`) } } } - if (publication_source_id === null) { + if (source_id === null) { throw new Error(`no sources for ref ${ref}`) } const { data: pub_data } = useReplicationPublicationsQuery({ projectRef: ref, - sourceId: publication_source_id, + sourceId: source_id, }) const publications = pub_data ?? [] @@ -56,6 +59,7 @@ export const ReplicationPublications = () => { text: 'Create a publication', }, }} + onClick={() => setShowCreatePublicationModal(true)} > New Publication @@ -64,9 +68,7 @@ export const ReplicationPublications = () => { Name, - - Num Tables - , + Num Tables, Edit, Delete, ]} @@ -96,6 +98,11 @@ export const ReplicationPublications = () => { + setShowCreatePublicationModal(false)} + /> ) } diff --git a/apps/studio/data/replication/create-publication-mutation.ts b/apps/studio/data/replication/create-publication-mutation.ts new file mode 100644 index 00000000000..8b721eb6862 --- /dev/null +++ b/apps/studio/data/replication/create-publication-mutation.ts @@ -0,0 +1,66 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import type { ResponseError } from 'types' +import { replicationKeys } from './keys' +import { handleError, post } from 'data/fetchers' + +export type CreatePublicationParams = { + projectRef: string + sourceId: number + name: string + tables: { schema: string; name: string }[] +} + +async function createPublication( + { projectRef, sourceId, name, tables }: CreatePublicationParams, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('projectRef is required') + + const { data, error } = await post( + '/platform/replication/{ref}/sources/{source_id}/publications', + { + params: { path: { ref: projectRef, source_id: sourceId } }, + body: { name, tables }, + signal, + } + ) + if (error) { + handleError(error) + } + + return data +} + +type CreatePublicationData = Awaited> + +export const useCreatePublicationMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => createPublication(vars), + { + async onSuccess(data, variables, context) { + const { projectRef, sourceId } = variables + await queryClient.invalidateQueries(replicationKeys.publications(projectRef, sourceId)) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to create publication: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/apps/studio/data/replication/create-source-mutation.ts b/apps/studio/data/replication/create-source-mutation.ts index 7710c8fb6e7..c68438bcf29 100644 --- a/apps/studio/data/replication/create-source-mutation.ts +++ b/apps/studio/data/replication/create-source-mutation.ts @@ -1,8 +1,6 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -// import { post } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' import type { ResponseError } from 'types' import { replicationKeys } from './keys' import { handleError, post } from 'data/fetchers' @@ -11,16 +9,6 @@ export type CreateSourceParams = { projectRef: string } -// export async function createSource({ projectRef }: CreateSourceParams) { -// if (!projectRef) throw new Error('projectRef is required') - -// const response = await post(`${API_URL}/storage/${projectRef}/buckets`, { -// id, -// }) -// if (response.error) throw response.error -// return response -// } - async function createSource({ projectRef }: CreateSourceParams, signal?: AbortSignal) { if (!projectRef) throw new Error('projectRef is required') diff --git a/apps/studio/data/replication/keys.ts b/apps/studio/data/replication/keys.ts index 71f392cabd0..82b648c3ecb 100644 --- a/apps/studio/data/replication/keys.ts +++ b/apps/studio/data/replication/keys.ts @@ -2,4 +2,6 @@ export const replicationKeys = { sources: (projectRef: string | undefined) => ['projects', projectRef, 'sources'] as const, publications: (projectRef: string | undefined, source_id: number) => ['projects', projectRef, 'sources', source_id, 'publications'] as const, + tables: (projectRef: string | undefined, source_id: number) => + ['projects', projectRef, 'sources', source_id, 'tables'] as const, } diff --git a/apps/studio/data/replication/tables-query.ts b/apps/studio/data/replication/tables-query.ts new file mode 100644 index 00000000000..58706c61f4b --- /dev/null +++ b/apps/studio/data/replication/tables-query.ts @@ -0,0 +1,36 @@ +import { UseQueryOptions, useQuery } from '@tanstack/react-query' + +import { get, handleError } from 'data/fetchers' +import { ResponseError } from 'types' +import { replicationKeys } from './keys' + +type ReplicationTablesParams = { projectRef?: string; sourceId: number } + +async function fetchReplicationTables( + { projectRef, sourceId }: ReplicationTablesParams, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('projectRef is required') + + const { data, error } = await get('/platform/replication/{ref}/sources/{source_id}/tables', { + params: { path: { ref: projectRef, source_id: sourceId } }, + signal, + }) + if (error) { + handleError(error) + } + + return data +} + +export type ReplicationTablesData = Awaited> + +export const useReplicationTablesQuery = ( + { projectRef, sourceId }: ReplicationTablesParams, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + replicationKeys.tables(projectRef, sourceId), + ({ signal }) => fetchReplicationTables({ projectRef, sourceId }, signal), + { enabled: enabled && typeof projectRef !== 'undefined', ...options } + ) diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index d19d5a82d15..2e960d6adb7 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -3328,6 +3328,12 @@ export interface paths { '/platform/replication/{ref}/sources/{source_id}/publications': { /** Gets source publications */ get: operations['ReplicationSourcesController_getPublications'] + /** Creates a publication */ + post: operations['ReplicationSourcesController_createPublication'] + } + '/platform/replication/{ref}/sources/{source_id}/tables': { + /** Gets source tables */ + get: operations['ReplicationSourcesController_getTables'] } '/platform/reset-password': { parameters: { @@ -8636,6 +8642,30 @@ export interface components { publish_update?: boolean tables?: string[] | null } + CreatePublicationResponse: Record + CreateReplicationPublicationBody: { + /** @description Publication name */ + name: string + /** @description Publication tables */ + tables: components['schemas']['ReplicationTable'][] + } + CreateRoleBody: { + admins?: string[] + can_bypass_rls?: boolean + can_create_db?: boolean + can_create_role?: boolean + can_login?: boolean + config?: Record + connection_limit?: number + inherit_role?: boolean + is_replication_role?: boolean + is_superuser?: boolean + member_of?: string[] + members?: string[] + name: string + password?: string + valid_until?: string + } CreateSchemaBody: { name: string owner: string @@ -10709,10 +10739,6 @@ export interface components { saml?: components['schemas']['SamlDescriptor'] updated_at?: string } - Publication: { - name: string - tables: components['schemas']['Table'][] - } PublicUrlOptions: { download?: boolean downloadName?: string @@ -10764,6 +10790,10 @@ export interface components { slot_name: string username: string } + ReplicationPublication: { + name: string + tables: components['schemas']['ReplicationTable'][] + } ReplicationSourcesResponse: { config: { Postgres?: components['schemas']['ReplicationPostgresConfig'] @@ -10771,6 +10801,10 @@ export interface components { id: number tenant_id: string } + ReplicationTable: { + name: string + schema: string + } ReportStatusBody: { databaseIdentifier: string reportingToken: string @@ -21620,7 +21654,7 @@ export interface operations { responses: { 200: { content: { - 'application/json': components['schemas']['Publication'][] + 'application/json': components['schemas']['ReplicationPublication'][] } } /** @description Failed to get source publications */ @@ -21629,6 +21663,55 @@ export interface operations { } } } + /** Creates a publication */ + ReplicationSourcesController_createPublication: { + parameters: { + path: { + /** @description Project ref */ + ref: string + /** @description Source id */ + source_id: number + } + } + requestBody: { + content: { + 'application/json': components['schemas']['CreateReplicationPublicationBody'] + } + } + responses: { + 201: { + content: { + 'application/json': components['schemas']['CreatePublicationResponse'] + } + } + /** @description Failed to create publication */ + 500: { + content: never + } + } + } + /** Gets source tables */ + ReplicationSourcesController_getTables: { + parameters: { + path: { + /** @description Project ref */ + ref: string + /** @description Source id */ + source_id: number + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['ReplicationTable'][] + } + } + /** @description Failed to get source tables */ + 500: { + content: never + } + } + } /** Reset password for email */ ResetPasswordController_resetPassword: { parameters: {