diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx index d5c29395f786..6d45c6605fc0 100644 --- a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx @@ -1,35 +1,23 @@ -import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import classNames from "classnames"; -import { Form, Formik, FieldArray, FormikHelpers } from "formik"; -import React, { ReactNode } from "react"; -import { FormattedMessage, useIntl } from "react-intl"; -import { Link } from "react-router-dom"; +import React from "react"; +import { FormattedMessage } from "react-intl"; -import { FormChangeTracker } from "components/common/FormChangeTracker"; -import { Button } from "components/ui/Button"; import { Card } from "components/ui/Card"; -import { DropdownMenu } from "components/ui/DropdownMenu"; import { Text } from "components/ui/Text"; import { WebBackendConnectionRead } from "core/request/AirbyteClient"; import { TrackErrorFn, useAppMonitoringService } from "hooks/services/AppMonitoringService"; -import { DbtCloudJob, isSameJob, useDbtIntegration, useAvailableDbtJobs } from "packages/cloud/services/dbtCloud"; -import { RoutePaths } from "pages/routePaths"; +import { useDbtIntegration, useAvailableDbtJobs } from "packages/cloud/services/dbtCloud"; import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; -import dbtLogo from "./dbt-bit_tm.svg"; -import styles from "./DbtCloudTransformationsCard.module.scss"; -import octaviaWorker from "./octavia-worker.png"; - -interface DbtJobListValues { - jobs: DbtCloudJob[]; -} +import styles from "./DbtCloudTransformationsCard/DbtCloudTransformationsCard.module.scss"; +import { DbtJobsForm } from "./DbtCloudTransformationsCard/DbtJobsForm"; +import { NoDbtIntegration } from "./DbtCloudTransformationsCard/NoDbtIntegration"; interface DbtCloudErrorBoundaryProps { trackError: TrackErrorFn; workspaceId: string; } + class DbtCloudErrorBoundary extends React.Component> { state = { error: null, displayMessage: null }; @@ -55,12 +43,12 @@ class DbtCloudErrorBoundary extends React.Component + } > - + {displayMessage ? ( ) : ( @@ -87,198 +75,20 @@ export const DbtCloudTransformationsCard = ({ connection }: { connection: WebBac // THEN show the jobs list and the "+ Add transformation" button const { hasDbtIntegration, isSaving, saveJobs, dbtCloudJobs } = useDbtIntegration(connection); + const availableDbtJobs = useAvailableDbtJobs(); const { trackError } = useAppMonitoringService(); const workspaceId = useCurrentWorkspaceId(); return hasDbtIntegration ? ( - + ) : ( ); }; - -const NoDbtIntegration = () => { - const workspaceId = useCurrentWorkspaceId(); - const dbtSettingsPath = `/${RoutePaths.Workspaces}/${workspaceId}/${RoutePaths.Settings}/dbt-cloud`; - return ( - - - - } - > -
- - {linkText}, - }} - /> - -
-
- ); -}; - -interface DbtJobsFormProps { - saveJobs: (jobs: DbtCloudJob[]) => Promise; - isSaving: boolean; - dbtCloudJobs: DbtCloudJob[]; -} -const DbtJobsForm: React.FC = ({ saveJobs, isSaving, dbtCloudJobs }) => { - const onSubmit = (values: DbtJobListValues, { resetForm }: FormikHelpers) => { - saveJobs(values.jobs).then(() => resetForm({ values })); - }; - - const availableDbtJobs = useAvailableDbtJobs(); - // because we don't store names for saved jobs, just the account and job IDs needed for - // webhook operation, we have to find the display names for saved jobs by comparing IDs - // with the list of available jobs as provided by dbt Cloud. - const jobs = dbtCloudJobs.map((savedJob) => { - const { jobName } = availableDbtJobs.find((remoteJob) => isSameJob(remoteJob, savedJob)) || {}; - const { accountId, jobId } = savedJob; - - return { accountId, jobId, jobName }; - }); - - return ( - { - return ( -
- - { - return ( - - - !values.jobs.some((savedJob) => isSameJob(remoteJob, savedJob))) - .map((job) => ({ displayName: job.jobName, value: job }))} - onChange={(selection) => { - push(selection.value); - }} - > - {() => ( - - )} - - - } - > - - - ); - }} - /> - - ); - }} - /> - ); -}; - -interface DbtJobsListProps { - jobs: DbtCloudJob[]; - remove: (i: number) => void; - dirty: boolean; - isLoading: boolean; -} - -const DbtJobsList = ({ jobs, remove, dirty, isLoading }: DbtJobsListProps) => { - const { formatMessage } = useIntl(); - - return ( -
- {jobs.length ? ( - <> - - - - {jobs.map((job, i) => ( - remove(i)} isLoading={isLoading} /> - ))} - - ) : ( - <> - - - - )} -
- - -
-
- ); -}; - -interface JobsListItemProps { - job: DbtCloudJob; - removeJob: () => void; - isLoading: boolean; -} -const JobsListItem = ({ job, removeJob, isLoading }: JobsListItemProps) => { - const { formatMessage } = useIntl(); - // TODO if `job.jobName` is undefined, that means we failed to match any of the - // dbt-Cloud-supplied jobs with the saved job. This means one of two things has - // happened: - // 1) the user deleted the job in dbt Cloud, and we should make them delete it from - // their webhook operations. If we have a nonempty list of other dbt Cloud jobs, - // it's definitely this. - // 2) the API call to fetch the names failed somehow (possibly with a 200 status, if there's a bug) - const title = {job.jobName || formatMessage({ id: "connection.dbtCloudJobs.job.title" })}; - - return ( - -
- - {title} -
-
-
- - {formatMessage({ id: "connection.dbtCloudJobs.job.accountId" })}: {job.accountId} - -
-
- - {formatMessage({ id: "connection.dbtCloudJobs.job.jobId" })}: {job.jobId} - -
- -
-
- ); -}; diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtCloudCard.scss b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtCloudCard.scss new file mode 100644 index 000000000000..0656f6acb9ff --- /dev/null +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtCloudCard.scss @@ -0,0 +1,24 @@ +@use "scss/colors"; +@use "scss/variables"; + +.cardTitle { + display: flex; + justify-content: space-between; +} + +.cardBodyContainer { + display: flex; + flex-direction: column; + align-items: center; + padding: variables.$spacing-xl; + background-color: colors.$grey-50; +} + +.contextExplanation { + color: colors.$grey-300; + width: 100%; + + & a { + color: colors.$grey-300; + } +} diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtCloudTransformationsCard.module.scss b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtCloudTransformationsCard.module.scss new file mode 100644 index 000000000000..3fcc280205d0 --- /dev/null +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtCloudTransformationsCard.module.scss @@ -0,0 +1 @@ +@forward "./DbtCloudCard.scss"; diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtJobsForm.module.scss b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtJobsForm.module.scss new file mode 100644 index 000000000000..96c9182596da --- /dev/null +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtJobsForm.module.scss @@ -0,0 +1,5 @@ +@forward "./DbtCloudCard.scss"; + +.jobListForm { + width: 100%; +} diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtJobsForm.tsx b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtJobsForm.tsx new file mode 100644 index 000000000000..459df0ddc8dd --- /dev/null +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtJobsForm.tsx @@ -0,0 +1,91 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Form, Formik, FieldArray, FormikHelpers } from "formik"; +import { FormattedMessage } from "react-intl"; + +import { FormChangeTracker } from "components/common/FormChangeTracker"; +import { Button } from "components/ui/Button"; +import { Card } from "components/ui/Card"; +import { DropdownMenu } from "components/ui/DropdownMenu"; + +import { DbtCloudJobInfo } from "packages/cloud/lib/domain/dbtCloud"; +import { DbtCloudJob, isSameJob } from "packages/cloud/services/dbtCloud"; + +import styles from "./DbtJobsForm.module.scss"; +import { JobsList } from "./JobsList"; + +interface DbtJobListValues { + jobs: DbtCloudJob[]; +} + +interface DbtJobsFormProps { + saveJobs: (jobs: DbtCloudJob[]) => Promise; + isSaving: boolean; + dbtCloudJobs: DbtCloudJob[]; + availableDbtCloudJobs: DbtCloudJobInfo[]; +} + +export const DbtJobsForm: React.FC = ({ + saveJobs, + isSaving, + dbtCloudJobs, + availableDbtCloudJobs, +}) => { + const onSubmit = (values: DbtJobListValues, { resetForm }: FormikHelpers) => { + saveJobs(values.jobs).then(() => resetForm({ values })); + }; + + // because we don't store names for saved jobs, just the account and job IDs needed for + // webhook operation, we have to find the display names for saved jobs by comparing IDs + // with the list of available jobs as provided by dbt Cloud. + const jobs = dbtCloudJobs.map((savedJob) => { + const { jobName } = availableDbtCloudJobs.find((remoteJob) => isSameJob(remoteJob, savedJob)) || {}; + const { accountId, jobId } = savedJob; + + return { accountId, jobId, jobName }; + }); + + return ( + { + return ( +
+ + { + return ( + + + !values.jobs.some((savedJob) => isSameJob(remoteJob, savedJob))) + .map((job) => ({ displayName: job.jobName, value: job }))} + onChange={(selection) => { + push(selection.value); + }} + > + {() => ( + + )} + + + } + > + + + ); + }} + /> + + ); + }} + /> + ); +}; diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsList.module.scss b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsList.module.scss new file mode 100644 index 000000000000..3ba64f83978e --- /dev/null +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsList.module.scss @@ -0,0 +1,20 @@ +@use "scss/variables"; + +@forward "./DbtCloudCard.scss"; + +.emptyListImage { + width: 111px; + height: 111px; + margin: variables.$spacing-xl 0; +} + +.jobListButtonGroup { + display: flex; + justify-content: flex-end; + margin-top: variables.$spacing-xl; + width: 100%; +} + +.jobListButton { + margin-left: variables.$spacing-md; +} diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsList.tsx b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsList.tsx new file mode 100644 index 000000000000..5ba001f40e2f --- /dev/null +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsList.tsx @@ -0,0 +1,56 @@ +import classNames from "classnames"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { Button } from "components/ui/Button"; +import { Text } from "components/ui/Text"; + +import { DbtCloudJob } from "packages/cloud/services/dbtCloud"; + +import styles from "./JobsList.module.scss"; +import { JobsListItem } from "./JobsListItem"; +import octaviaWorker from "./octavia-worker.png"; + +interface JobsListProps { + jobs: DbtCloudJob[]; + remove: (i: number) => void; + dirty: boolean; + isLoading: boolean; +} + +export const JobsList = ({ jobs, remove, dirty, isLoading }: JobsListProps) => { + const { formatMessage } = useIntl(); + + return ( +
+ {jobs.length ? ( + <> + + + + {jobs.map((job, i) => ( + remove(i)} isLoading={isLoading} /> + ))} + + ) : ( + <> + + + + )} +
+ + +
+
+ ); +}; diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.module.scss b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsListItem.module.scss similarity index 62% rename from airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.module.scss rename to airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsListItem.module.scss index 9d9a85316dbc..9a1996378af4 100644 --- a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.module.scss +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsListItem.module.scss @@ -1,49 +1,6 @@ @use "scss/colors"; @use "scss/variables"; -.jobListContainer { - display: flex; - flex-direction: column; - align-items: center; - padding: variables.$spacing-xl; - background-color: colors.$grey-50; -} - -.jobListTitle { - display: flex; - justify-content: space-between; -} - -.jobListForm { - width: 100%; -} - -.emptyListImage { - width: 111px; - height: 111px; - margin: variables.$spacing-xl 0; -} - -.contextExplanation { - color: colors.$grey-300; - width: 100%; - - & a { - color: colors.$grey-300; - } -} - -.jobListButtonGroup { - display: flex; - justify-content: flex-end; - margin-top: variables.$spacing-xl; - width: 100%; -} - -.jobListButton { - margin-left: variables.$spacing-md; -} - .jobListItem { margin-top: variables.$spacing-md; padding: variables.$spacing-md variables.$spacing-xl; @@ -53,18 +10,18 @@ align-items: center; } -.dbtLogo { - height: 18px; - width: 18px; - margin-right: variables.$spacing-md; -} - .jobListItemIntegrationName { display: flex; align-items: center; flex: 1 2 auto; } +.dbtLogo { + height: 18px; + width: 18px; + margin-right: variables.$spacing-md; +} + .jobListItemIdFieldGroup { display: flex; justify-content: space-between; diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsListItem.tsx b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsListItem.tsx new file mode 100644 index 000000000000..fffa743808cb --- /dev/null +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsListItem.tsx @@ -0,0 +1,60 @@ +import { faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useIntl } from "react-intl"; + +import { Button } from "components/ui/Button"; +import { Card } from "components/ui/Card"; +import { Text } from "components/ui/Text"; + +import { DbtCloudJob } from "packages/cloud/services/dbtCloud"; + +import dbtLogo from "./dbt-bit_tm.svg"; +import styles from "./JobsListItem.module.scss"; + +interface JobsListItemProps { + job: DbtCloudJob; + removeJob: () => void; + isLoading: boolean; +} +export const JobsListItem = ({ job, removeJob, isLoading }: JobsListItemProps) => { + const { formatMessage } = useIntl(); + // TODO if `job.jobName` is undefined, that means we failed to match any of the + // dbt-Cloud-supplied jobs with the saved job. This means one of two things has + // happened: + // 1) the user deleted the job in dbt Cloud, and we should make them delete it from + // their webhook operations. If we have a nonempty list of other dbt Cloud jobs, + // it's definitely this. + // 2) the API call to fetch the names failed somehow (possibly with a 200 status, if there's a bug) + const title = {job.jobName || formatMessage({ id: "connection.dbtCloudJobs.job.title" })}; + + return ( + +
+ + {title} +
+
+
+ + {formatMessage({ id: "connection.dbtCloudJobs.job.accountId" })}: {job.accountId} + +
+
+ + {formatMessage({ id: "connection.dbtCloudJobs.job.jobId" })}: {job.jobId} + +
+ +
+
+ ); +}; diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/NoDbtIntegration.module.scss b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/NoDbtIntegration.module.scss new file mode 100644 index 000000000000..3fcc280205d0 --- /dev/null +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/NoDbtIntegration.module.scss @@ -0,0 +1 @@ +@forward "./DbtCloudCard.scss"; diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/NoDbtIntegration.tsx b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/NoDbtIntegration.tsx new file mode 100644 index 000000000000..ee242e9ac2a7 --- /dev/null +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/NoDbtIntegration.tsx @@ -0,0 +1,37 @@ +import classNames from "classnames"; +import { ReactNode } from "react"; +import { FormattedMessage } from "react-intl"; +import { Link } from "react-router-dom"; + +import { Card } from "components/ui/Card"; +import { Text } from "components/ui/Text"; + +import { RoutePaths } from "pages/routePaths"; +import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; + +import styles from "./NoDbtIntegration.module.scss"; + +export const NoDbtIntegration = () => { + const workspaceId = useCurrentWorkspaceId(); + const dbtSettingsPath = `/${RoutePaths.Workspaces}/${workspaceId}/${RoutePaths.Settings}/dbt-cloud`; + return ( + + + + } + > +
+ + {linkText}, + }} + /> + +
+
+ ); +}; diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/dbt-bit_tm.svg b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/dbt-bit_tm.svg similarity index 100% rename from airbyte-webapp/src/pages/connections/ConnectionTransformationPage/dbt-bit_tm.svg rename to airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/dbt-bit_tm.svg diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/octavia-worker.png b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/octavia-worker.png similarity index 100% rename from airbyte-webapp/src/pages/connections/ConnectionTransformationPage/octavia-worker.png rename to airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/octavia-worker.png