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()}
+ >
+
+
+ )
+}
+
+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: {