From b4db50e6cbfc8816cc854dc4749d25fb4103d0d7 Mon Sep 17 00:00:00 2001 From: Alex Birdsall Date: Fri, 16 Dec 2022 16:26:46 -0800 Subject: [PATCH 1/5] Add WIP DbtCloudErrorBoundary --- .../src/packages/cloud/locales/en.json | 2 + .../DbtCloudTransformationsCard.tsx | 50 ++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/airbyte-webapp/src/packages/cloud/locales/en.json b/airbyte-webapp/src/packages/cloud/locales/en.json index e154f02eb869..7858df4591e3 100644 --- a/airbyte-webapp/src/packages/cloud/locales/en.json +++ b/airbyte-webapp/src/packages/cloud/locales/en.json @@ -61,6 +61,8 @@ "connection.dbtCloudJobs.cardTitle": "Transformations", "connection.dbtCloudJobs.addJob": "Add transformation", + "connection.dbtCloudJobs.dbtError": "There was an error communicating with dbt Cloud: {displayMessage}", + "connection.dbtCloudJobs.genericError": "There was an error communicating with dbt Cloud.", "connection.dbtCloudJobs.explanation": "After an Airbyte sync job has completed, the following jobs will run", "connection.dbtCloudJobs.noJobs": "No transformations", "connection.dbtCloudJobs.job.title": "dbt Cloud transform", diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx index af7a6048dbf9..c07cf9a654da 100644 --- a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx @@ -2,7 +2,7 @@ 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 { ReactNode } from "react"; +import React, { ReactNode } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { Link } from "react-router-dom"; @@ -25,6 +25,50 @@ interface DbtJobListValues { jobs: DbtCloudJob[]; } +const DbtCloudErrorBoundary = class extends React.Component { + state = { error: null, displayMessage: null }; + + // TODO parse the error to determine if the source was the upstream network call to + // the dbt Cloud API. If it is, extract the `user_message` field from dbt's error + // response for display to user; if not, provide a more generic error message. If the + // error was *definitely* not related to the dbt Cloud API, consider reraising it. + static getDerivedStateFromError(error: Error) { + // TODO I'm pretty sure I did not correctly mock the exact error response format. + // eslint-disable-next-line + const displayMessage = (error?.message as any)?.status?.user_message; + return { error, displayMessage }; + } + + componentDidCatch(error: Error) { + console.log(error); + } + + render() { + const { error, displayMessage } = this.state; + if (error) { + return ( + + + + } + > + + {displayMessage ? ( + + ) : ( + + )} + + + ); + } + + return this.props.children; + } +}; + export const DbtCloudTransformationsCard = ({ connection }: { connection: WebBackendConnectionRead }) => { // Possible render paths: // 1) IF the workspace has no dbt cloud account linked @@ -39,7 +83,9 @@ export const DbtCloudTransformationsCard = ({ connection }: { connection: WebBac const { hasDbtIntegration, isSaving, saveJobs, dbtCloudJobs } = useDbtIntegration(connection); return hasDbtIntegration ? ( - + + + ) : ( ); From 3f6b280b12fbed920bed67f6b6fbe10ef9263f2e Mon Sep 17 00:00:00 2001 From: Alex Birdsall Date: Mon, 30 Jan 2023 15:30:30 -0800 Subject: [PATCH 2/5] get workspaceId via useCurrentWorkspaceId It's a bit more efficient and I don't need any of the other things provided by `useCurrentWorkspace`. --- .../DbtCloudTransformationsCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx index c07cf9a654da..6a85d5e3876d 100644 --- a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx @@ -13,9 +13,9 @@ import { DropdownMenu } from "components/ui/DropdownMenu"; import { Text } from "components/ui/Text"; import { WebBackendConnectionRead } from "core/request/AirbyteClient"; -import { useCurrentWorkspace } from "hooks/services/useWorkspace"; import { DbtCloudJob, isSameJob, useDbtIntegration, useAvailableDbtJobs } from "packages/cloud/services/dbtCloud"; import { RoutePaths } from "pages/routePaths"; +import { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; import dbtLogo from "./dbt-bit_tm.svg"; import styles from "./DbtCloudTransformationsCard.module.scss"; @@ -92,7 +92,7 @@ export const DbtCloudTransformationsCard = ({ connection }: { connection: WebBac }; const NoDbtIntegration = () => { - const { workspaceId } = useCurrentWorkspace(); + const workspaceId = useCurrentWorkspaceId(); const dbtSettingsPath = `/${RoutePaths.Workspaces}/${workspaceId}/${RoutePaths.Settings}/dbt-cloud`; return ( Date: Mon, 30 Jan 2023 15:31:41 -0800 Subject: [PATCH 3/5] Track dbt Cloud errors via AppMonitoringService --- .../DbtCloudTransformationsCard.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx index 6a85d5e3876d..d5c29395f786 100644 --- a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx @@ -13,6 +13,7 @@ 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 { useCurrentWorkspaceId } from "services/workspaces/WorkspacesService"; @@ -25,7 +26,11 @@ interface DbtJobListValues { jobs: DbtCloudJob[]; } -const DbtCloudErrorBoundary = class extends React.Component { +interface DbtCloudErrorBoundaryProps { + trackError: TrackErrorFn; + workspaceId: string; +} +class DbtCloudErrorBoundary extends React.Component> { state = { error: null, displayMessage: null }; // TODO parse the error to determine if the source was the upstream network call to @@ -40,7 +45,8 @@ const DbtCloudErrorBoundary = class extends React.Component { } componentDidCatch(error: Error) { - console.log(error); + const { trackError, workspaceId } = this.props; + trackError(error, { workspaceId }); } render() { @@ -67,7 +73,7 @@ const DbtCloudErrorBoundary = class extends React.Component { return this.props.children; } -}; +} export const DbtCloudTransformationsCard = ({ connection }: { connection: WebBackendConnectionRead }) => { // Possible render paths: @@ -81,9 +87,11 @@ export const DbtCloudTransformationsCard = ({ connection }: { connection: WebBac // THEN show the jobs list and the "+ Add transformation" button const { hasDbtIntegration, isSaving, saveJobs, dbtCloudJobs } = useDbtIntegration(connection); + const { trackError } = useAppMonitoringService(); + const workspaceId = useCurrentWorkspaceId(); return hasDbtIntegration ? ( - + ) : ( From 04b8fb1cb119484e583ccc2b079b6b6bca26a95a Mon Sep 17 00:00:00 2001 From: Alex Birdsall Date: Tue, 31 Jan 2023 16:41:29 -0800 Subject: [PATCH 4/5] Extract UI helper components from DbtCloudTransformationsCard.tsx I put a little effort into keeping all of the API interactions within the top-level component, and a linting rule required me to split out individual stylesheets for each helper component (plus one non-module scss file for shared card styles that individual scss modules can `@forward`) --- .../DbtCloudTransformationsCard.tsx | 222 ++---------------- .../DbtCloudCard.scss | 24 ++ .../DbtCloudTransformationsCard.module.scss | 1 + .../DbtJobsForm.module.scss | 5 + .../DbtJobsForm.tsx | 91 +++++++ .../JobsList.module.scss | 20 ++ .../DbtCloudTransformationsCard/JobsList.tsx | 56 +++++ .../JobsListItem.module.scss} | 55 +---- .../JobsListItem.tsx | 60 +++++ .../NoDbtIntegration.module.scss | 1 + .../NoDbtIntegration.tsx | 37 +++ .../dbt-bit_tm.svg | 0 .../octavia-worker.png | Bin 13 files changed, 317 insertions(+), 255 deletions(-) create mode 100644 airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtCloudCard.scss create mode 100644 airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtCloudTransformationsCard.module.scss create mode 100644 airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtJobsForm.module.scss create mode 100644 airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/DbtJobsForm.tsx create mode 100644 airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsList.module.scss create mode 100644 airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsList.tsx rename airbyte-webapp/src/pages/connections/ConnectionTransformationPage/{DbtCloudTransformationsCard.module.scss => DbtCloudTransformationsCard/JobsListItem.module.scss} (62%) create mode 100644 airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/JobsListItem.tsx create mode 100644 airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/NoDbtIntegration.module.scss create mode 100644 airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard/NoDbtIntegration.tsx rename airbyte-webapp/src/pages/connections/ConnectionTransformationPage/{ => DbtCloudTransformationsCard}/dbt-bit_tm.svg (100%) rename airbyte-webapp/src/pages/connections/ConnectionTransformationPage/{ => DbtCloudTransformationsCard}/octavia-worker.png (100%) 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 From f10a7ac48d4c81a5c9c444184002149ed1bc0fc9 Mon Sep 17 00:00:00 2001 From: Alex Birdsall Date: Tue, 31 Jan 2023 17:06:39 -0800 Subject: [PATCH 5/5] Put available jobs query within error boundary --- .../DbtCloudTransformationsCard.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx index 6d45c6605fc0..bc37bbfdb641 100644 --- a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/DbtCloudTransformationsCard.tsx @@ -63,6 +63,20 @@ class DbtCloudErrorBoundary extends React.Component, "hasDbtIntegration">; + +const DbtIntegrationCardContent = ({ saveJobs, isSaving, dbtCloudJobs }: DbtIntegrationCardContentProps) => { + const availableDbtJobs = useAvailableDbtJobs(); + return ( + + ); +}; + export const DbtCloudTransformationsCard = ({ connection }: { connection: WebBackendConnectionRead }) => { // Possible render paths: // 1) IF the workspace has no dbt cloud account linked @@ -75,18 +89,12 @@ 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 ? ( - + ) : (