From d7eafe547f535b7aabebe03368450839287ab59c Mon Sep 17 00:00:00 2001 From: Artem Astapenko <3767150+Jamakase@users.noreply.github.com> Date: Fri, 9 Jul 2021 21:53:45 +0300 Subject: [PATCH] Combine admin and settings (#4525) * Add side menu component * Add side menu to settings page. Remove admin link from sidebar * Move NotificationPage * Move ConfigurationPage * Add Sources and Destinations pages to Settings. Delete Admin page * Add MetricsPage * Edit Notifications and Metrics pages * Update feedback for metrics and notification pages * Add update icons data to side menu * Add AccountPage --- .../src/components/SideMenu/SideMenu.tsx | 38 ++++ .../SideMenu/components/MenuItem.tsx | 51 +++++ .../src/components/SideMenu/index.tsx | 4 + .../hooks/services/useConnector.tsx | 21 ++ .../hooks/services/useWorkspaceHook.tsx | 1 + airbyte-webapp/src/components/index.tsx | 1 + airbyte-webapp/src/locales/en.json | 8 + .../src/pages/AdminPage/AdminPage.tsx | 83 ------- .../AdminPage/components/DestinationsView.tsx | 203 ------------------ airbyte-webapp/src/pages/AdminPage/index.tsx | 3 - .../src/pages/SettingsPage/SettingsPage.tsx | 86 +++++++- .../SettingsPage/components/FeedbackBlock.tsx | 49 +++++ .../components/useWorkspaceEditor.tsx | 53 +++++ .../pages/AccountPage/AccountPage.tsx | 58 +++++ .../AccountPage/components/AccountForm.tsx | 112 ++++++++++ .../SettingsPage/pages/AccountPage/index.tsx | 3 + .../ConfigurationsPage.tsx} | 15 +- .../components/ImportConfigurationModal.tsx | 0 .../components/LogsContent.tsx | 0 .../pages/ConfigurationsPage/index.tsx | 3 + .../pages/ConnectorsPage/DestinationsPage.tsx | 104 +++++++++ .../pages/ConnectorsPage/SourcesPage.tsx | 98 +++++++++ .../components/ConnectorCell.tsx | 42 ++++ .../components/ConnectorsView.tsx | 167 ++++++++++++++ .../components/CreateConnector.tsx | 2 +- .../components/CreateConnectorModal.tsx | 0 .../ConnectorsPage}/components/ImageCell.tsx | 0 .../components/PageComponents.tsx | 0 .../components/UpgradeAllButton.tsx | 74 +++++++ .../components/VersionCell.tsx | 0 .../pages/ConnectorsPage/index.tsx | 4 + .../pages/MetricsPage/MetricsPage.tsx | 59 +++++ .../MetricsPage/components/MetricsForm.tsx | 87 ++++++++ .../SettingsPage/pages/MetricsPage/index.tsx | 3 + .../NotificationPage/NotificationPage.tsx} | 56 +++-- .../components/NotificationsForm.tsx | 76 +++++++ .../components/WebHookForm.tsx | 4 +- .../pages/NotificationPage/index.tsx | 3 + .../pages/SourcesPage/SourcesPage.tsx} | 14 +- .../SourcesPage}/components/ConnectorCell.tsx | 0 .../SourcesPage/components/ImageCell.tsx | 28 +++ .../SourcesPage/components/PageComponents.tsx | 26 +++ .../components/UpgradeAllButton.tsx | 0 .../SourcesPage/components/VersionCell.tsx | 138 ++++++++++++ .../SettingsPage/pages/SourcesPage/index.tsx | 3 + airbyte-webapp/src/pages/routes.tsx | 26 ++- .../src/views/layout/SideBar/SideBar.tsx | 29 +-- 47 files changed, 1465 insertions(+), 370 deletions(-) create mode 100644 airbyte-webapp/src/components/SideMenu/SideMenu.tsx create mode 100644 airbyte-webapp/src/components/SideMenu/components/MenuItem.tsx create mode 100644 airbyte-webapp/src/components/SideMenu/index.tsx delete mode 100644 airbyte-webapp/src/pages/AdminPage/AdminPage.tsx delete mode 100644 airbyte-webapp/src/pages/AdminPage/components/DestinationsView.tsx delete mode 100644 airbyte-webapp/src/pages/AdminPage/index.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/components/FeedbackBlock.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/components/useWorkspaceEditor.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/AccountPage/AccountPage.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/AccountPage/components/AccountForm.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/AccountPage/index.tsx rename airbyte-webapp/src/pages/{AdminPage/components/ConfigurationView.tsx => SettingsPage/pages/ConfigurationsPage/ConfigurationsPage.tsx} (90%) rename airbyte-webapp/src/pages/{AdminPage => SettingsPage/pages/ConfigurationsPage}/components/ImportConfigurationModal.tsx (100%) rename airbyte-webapp/src/pages/{AdminPage => SettingsPage/pages/ConfigurationsPage}/components/LogsContent.tsx (100%) create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/ConfigurationsPage/index.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/DestinationsPage.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/SourcesPage.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorCell.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorsView.tsx rename airbyte-webapp/src/pages/{AdminPage => SettingsPage/pages/ConnectorsPage}/components/CreateConnector.tsx (98%) rename airbyte-webapp/src/pages/{AdminPage => SettingsPage/pages/ConnectorsPage}/components/CreateConnectorModal.tsx (100%) rename airbyte-webapp/src/pages/{AdminPage => SettingsPage/pages/ConnectorsPage}/components/ImageCell.tsx (100%) rename airbyte-webapp/src/pages/{AdminPage => SettingsPage/pages/ConnectorsPage}/components/PageComponents.tsx (100%) create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/UpgradeAllButton.tsx rename airbyte-webapp/src/pages/{AdminPage => SettingsPage/pages/ConnectorsPage}/components/VersionCell.tsx (100%) create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/index.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/MetricsPage/MetricsPage.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/MetricsPage/components/MetricsForm.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/MetricsPage/index.tsx rename airbyte-webapp/src/pages/SettingsPage/{components/AccountSettings.tsx => pages/NotificationPage/NotificationPage.tsx} (64%) create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/components/NotificationsForm.tsx rename airbyte-webapp/src/pages/SettingsPage/{ => pages/NotificationPage}/components/WebHookForm.tsx (98%) create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/index.tsx rename airbyte-webapp/src/pages/{AdminPage/components/SourcesView.tsx => SettingsPage/pages/SourcesPage/SourcesPage.tsx} (93%) rename airbyte-webapp/src/pages/{AdminPage => SettingsPage/pages/SourcesPage}/components/ConnectorCell.tsx (100%) create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/ImageCell.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/PageComponents.tsx rename airbyte-webapp/src/pages/{AdminPage => SettingsPage/pages/SourcesPage}/components/UpgradeAllButton.tsx (100%) create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/VersionCell.tsx create mode 100644 airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/index.tsx diff --git a/airbyte-webapp/src/components/SideMenu/SideMenu.tsx b/airbyte-webapp/src/components/SideMenu/SideMenu.tsx new file mode 100644 index 000000000000..5477cb068ff1 --- /dev/null +++ b/airbyte-webapp/src/components/SideMenu/SideMenu.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import styled from "styled-components"; + +import MenuItem from "./components/MenuItem"; + +export type SideMenuItem = { + id: string; + name: string | React.ReactNode; + indicatorCount?: number; +}; + +type SideMenuProps = { + data: SideMenuItem[]; + activeItem?: string; + onSelect: (id: string) => void; +}; + +const Content = styled.nav` + min-width: 147px; +`; + +const SideMenu: React.FC = ({ data, onSelect, activeItem }) => { + return ( + + {data.map((item) => ( + onSelect(item.id)} + /> + ))} + + ); +}; + +export default SideMenu; diff --git a/airbyte-webapp/src/components/SideMenu/components/MenuItem.tsx b/airbyte-webapp/src/components/SideMenu/components/MenuItem.tsx new file mode 100644 index 000000000000..583035d63142 --- /dev/null +++ b/airbyte-webapp/src/components/SideMenu/components/MenuItem.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import styled from "styled-components"; + +type IProps = { + name: string | React.ReactNode; + isActive?: boolean; + count?: number; + onClick: () => void; +}; + +const Item = styled.div<{ + isActive?: boolean; +}>` + width: 100%; + padding: 6px 8px 7px; + border-radius: 4px; + cursor: pointer; + background: ${({ theme, isActive }) => + isActive ? theme.primaryColor12 : "none"}; + font-style: normal; + font-weight: 500; + font-size: 12px; + line-height: 15px; + color: ${({ theme, isActive }) => + isActive ? theme.primaryColor : theme.greyColor60}; +`; + +const Counter = styled.div` + min-width: 12px; + height: 12px; + padding: 0 3px; + text-align: center; + border-radius: 15px; + background: ${({ theme }) => theme.dangerColor}; + font-size: 8px; + line-height: 13px; + color: ${({ theme }) => theme.whiteColor}; + display: inline-block; + margin-left: 5px; +`; + +const MenuItem: React.FC = ({ name, isActive, count, onClick }) => { + return ( + + {name} + {count ? {count} : null} + + ); +}; + +export default MenuItem; diff --git a/airbyte-webapp/src/components/SideMenu/index.tsx b/airbyte-webapp/src/components/SideMenu/index.tsx new file mode 100644 index 000000000000..203a285deccb --- /dev/null +++ b/airbyte-webapp/src/components/SideMenu/index.tsx @@ -0,0 +1,4 @@ +import SideMenu from "./SideMenu"; + +export default SideMenu; +export { SideMenu }; diff --git a/airbyte-webapp/src/components/hooks/services/useConnector.tsx b/airbyte-webapp/src/components/hooks/services/useConnector.tsx index 2a77f39c03c5..5b60b4f9aa76 100644 --- a/airbyte-webapp/src/components/hooks/services/useConnector.tsx +++ b/airbyte-webapp/src/components/hooks/services/useConnector.tsx @@ -9,6 +9,8 @@ type ConnectorService = { hasNewVersions: boolean; hasNewSourceVersion: boolean; hasNewDestinationVersion: boolean; + countNewSourceVersion: number; + countNewDestinationVersion: number; updateAllSourceVersions: () => void; updateAllDestinationVersions: () => void; }; @@ -57,6 +59,23 @@ const useConnector = (): ConnectorService => { [hasNewSourceVersion, hasNewDestinationVersion] ); + const countNewSourceVersion = useMemo( + () => + sourceDefinitions.filter( + (source) => source.latestDockerImageTag !== source.dockerImageTag + ).length, + [sourceDefinitions] + ); + + const countNewDestinationVersion = useMemo( + () => + destinationDefinitions.filter( + (destination) => + destination.latestDockerImageTag !== destination.dockerImageTag + ).length, + [destinationDefinitions] + ); + const updateAllSourceVersions = async () => { const updateList = sourceDefinitions.filter( (source) => source.latestDockerImageTag !== source.dockerImageTag @@ -100,6 +119,8 @@ const useConnector = (): ConnectorService => { hasNewDestinationVersion, updateAllSourceVersions, updateAllDestinationVersions, + countNewSourceVersion, + countNewDestinationVersion, }; }; diff --git a/airbyte-webapp/src/components/hooks/services/useWorkspaceHook.tsx b/airbyte-webapp/src/components/hooks/services/useWorkspaceHook.tsx index 1cc7bc446e2d..31d6227fc7a9 100644 --- a/airbyte-webapp/src/components/hooks/services/useWorkspaceHook.tsx +++ b/airbyte-webapp/src/components/hooks/services/useWorkspaceHook.tsx @@ -78,6 +78,7 @@ const useWorkspace = (): { workspaceId: config.ui.workspaceId, initialSetupComplete: workspace.initialSetupComplete, displaySetupWizard: workspace.displaySetupWizard, + notifications: workspace.notifications, ...data, } ); diff --git a/airbyte-webapp/src/components/index.tsx b/airbyte-webapp/src/components/index.tsx index 8bab89641b7f..a94cf6ba2922 100644 --- a/airbyte-webapp/src/components/index.tsx +++ b/airbyte-webapp/src/components/index.tsx @@ -17,3 +17,4 @@ export * from "./ContentCard"; export * from "./ImageBlock"; export * from "./LabeledRadioButton"; export * from "./Modal"; +export * from "./SideMenu"; diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index fb7644945e81..e77818b7f765 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -322,6 +322,14 @@ "settings.webhookTestText": "Testing the Webhook will send a “Hello World”. ", "settings.yourWebhook": "Your Webhook URL", "settings.test": "Test", + "settings.notifications": "Notifications", + "settings.metrics": "Metrics", + "settings.notificationSettings": "Notification Settings", + "settings.metricsSettings": "Metrics Settings", + "settings.emailNotifications": "Email notifications", + "settings.securityUpdates": "Security updates (recommended)", + "settings.newsletter": "Newsletter with feature updates.", + "settings.account": "Account", "connector.requestConnectorBlock": "+ Request a new connector", "connector.requestConnector": "Request a new connector", diff --git a/airbyte-webapp/src/pages/AdminPage/AdminPage.tsx b/airbyte-webapp/src/pages/AdminPage/AdminPage.tsx deleted file mode 100644 index 9cf6d2fa08d5..000000000000 --- a/airbyte-webapp/src/pages/AdminPage/AdminPage.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { Suspense, useState } from "react"; -import { FormattedMessage } from "react-intl"; -import styled from "styled-components"; - -import MainPageWithScroll from "components/MainPageWithScroll"; -import PageTitle from "components/PageTitle"; -import StepsMenu from "components/StepsMenu"; -import { StepMenuItem } from "components/StepsMenu/StepsMenu"; -import LoadingPage from "components/LoadingPage"; -import SourcesView from "./components/SourcesView"; -import DestinationsView from "./components/DestinationsView"; -import CreateConnector from "./components/CreateConnector"; -import ConfigurationView from "./components/ConfigurationView"; -import HeadTitle from "components/HeadTitle"; - -const Content = styled.div` - padding-top: 4px; - margin: 0 33px 0 27px; - height: 100%; -`; - -enum StepsTypes { - SOURCES = "sources", - DESTINATIONS = "destinations", - CONFIGURATION = "configuration", -} - -const AdminPage: React.FC = () => { - const steps: StepMenuItem[] = [ - { - id: StepsTypes.SOURCES, - name: , - }, - { - id: StepsTypes.DESTINATIONS, - name: , - }, - { - id: StepsTypes.CONFIGURATION, - name: , - }, - ]; - const [currentStep, setCurrentStep] = useState(StepsTypes.SOURCES); - const onSelectStep = (id: string) => setCurrentStep(id); - - const renderStep = () => { - if (currentStep === StepsTypes.SOURCES) { - return ; - } - if (currentStep === StepsTypes.CONFIGURATION) { - return ; - } - - return ; - }; - - return ( - } - pageTitle={ - } - middleComponent={ - - } - endComponent={} - /> - } - > - - }>{renderStep()} - - - ); -}; - -export default AdminPage; diff --git a/airbyte-webapp/src/pages/AdminPage/components/DestinationsView.tsx b/airbyte-webapp/src/pages/AdminPage/components/DestinationsView.tsx deleted file mode 100644 index ef99bb2dc208..000000000000 --- a/airbyte-webapp/src/pages/AdminPage/components/DestinationsView.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import React, { useCallback, useMemo, useState } from "react"; -import { FormattedMessage, useIntl } from "react-intl"; -import { useFetcher, useResource } from "rest-hooks"; -import { CellProps } from "react-table"; -import { useAsyncFn } from "react-use"; - -import { Block, Title, FormContentTitle } from "./PageComponents"; -import Table from "components/Table"; -import ConnectorCell from "./ConnectorCell"; -import ImageCell from "./ImageCell"; -import VersionCell from "./VersionCell"; -import config from "config"; -import DestinationDefinitionResource from "core/resources/DestinationDefinition"; -import { DestinationResource } from "core/resources/Destination"; -import { DestinationDefinition } from "core/resources/DestinationDefinition"; -import UpgradeAllButton from "./UpgradeAllButton"; -import useConnector from "components/hooks/services/useConnector"; -import HeadTitle from "components/HeadTitle"; - -const DestinationsView: React.FC = () => { - const [successUpdate, setSuccessUpdate] = useState(false); - const formatMessage = useIntl().formatMessage; - const { destinationDefinitions } = useResource( - DestinationDefinitionResource.listShape(), - { - workspaceId: config.ui.workspaceId, - } - ); - const { destinations } = useResource(DestinationResource.listShape(), { - workspaceId: config.ui.workspaceId, - }); - - const [feedbackList, setFeedbackList] = useState>({}); - - const updateDestinationDefinition = useFetcher( - DestinationDefinitionResource.updateShape() - ); - - const { hasNewDestinationVersion } = useConnector(); - - const onUpdateVersion = useCallback( - async ({ id, version }: { id: string; version: string }) => { - try { - await updateDestinationDefinition( - {}, - { - destinationDefinitionId: id, - dockerImageTag: version, - } - ); - setFeedbackList({ ...feedbackList, [id]: "success" }); - } catch (e) { - const messageId = - e.status === 422 ? "form.imageCannotFound" : "form.someError"; - setFeedbackList({ - ...feedbackList, - [id]: formatMessage({ id: messageId }), - }); - } - }, - [feedbackList, formatMessage, updateDestinationDefinition] - ); - - const columns = React.useMemo( - () => [ - { - Header: , - accessor: "name", - customWidth: 25, - Cell: ({ - cell, - row, - }: CellProps<{ - latestDockerImageTag: string; - dockerImageTag: string; - icon?: string; - }>) => ( - - ), - }, - { - Header: , - accessor: "dockerRepository", - customWidth: 36, - Cell: ({ cell, row }: CellProps<{ documentationUrl: string }>) => ( - - ), - }, - { - Header: , - accessor: "dockerImageTag", - customWidth: 10, - }, - { - Header: ( - - - - ), - accessor: "latestDockerImageTag", - collapse: true, - Cell: ({ - cell, - row, - }: CellProps<{ - destinationDefinitionId: string; - dockerImageTag: string; - }>) => ( - - ), - }, - ], - [feedbackList, onUpdateVersion] - ); - - const usedDestinationDefinitions = useMemo(() => { - const destinationDefinitionMap = new Map(); - destinations.forEach((destination) => { - const destinationDefinition = destinationDefinitions.find( - (destinationDefinition) => - destinationDefinition.destinationDefinitionId === - destination.destinationDefinitionId - ); - - if (destinationDefinition) { - destinationDefinitionMap.set( - destinationDefinition.destinationDefinitionId, - destinationDefinition - ); - } - }); - - return Array.from(destinationDefinitionMap.values()); - }, [destinations, destinationDefinitions]); - - const { updateAllDestinationVersions } = useConnector(); - - const [{ loading, error }, onUpdate] = useAsyncFn(async () => { - setSuccessUpdate(false); - await updateAllDestinationVersions(); - setSuccessUpdate(true); - setTimeout(() => { - setSuccessUpdate(false); - }, 2000); - }, [updateAllDestinationVersions]); - - return ( - <> - - {usedDestinationDefinitions.length ? ( - - - <FormattedMessage id="admin.manageDestination" /> - {(hasNewDestinationVersion || successUpdate) && ( - <UpgradeAllButton - isLoading={loading} - hasError={!!error && !loading} - hasSuccess={successUpdate} - onUpdate={onUpdate} - /> - )} - - - - ) : null} - - - - <FormattedMessage id="admin.availableDestinations" /> - {(hasNewDestinationVersion || successUpdate) && - !usedDestinationDefinitions.length && ( - <UpgradeAllButton - isLoading={loading} - hasError={!!error && !loading} - hasSuccess={successUpdate} - onUpdate={onUpdate} - /> - )} - -
- - - ); -}; - -export default DestinationsView; diff --git a/airbyte-webapp/src/pages/AdminPage/index.tsx b/airbyte-webapp/src/pages/AdminPage/index.tsx deleted file mode 100644 index d2e0bbd197f6..000000000000 --- a/airbyte-webapp/src/pages/AdminPage/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import AdminPage from "./AdminPage"; - -export default AdminPage; diff --git a/airbyte-webapp/src/pages/SettingsPage/SettingsPage.tsx b/airbyte-webapp/src/pages/SettingsPage/SettingsPage.tsx index c9ab4a27cc22..e3a2587bf96b 100644 --- a/airbyte-webapp/src/pages/SettingsPage/SettingsPage.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/SettingsPage.tsx @@ -1,19 +1,68 @@ import React, { Suspense } from "react"; import { FormattedMessage } from "react-intl"; import styled from "styled-components"; +import { Redirect, Route, Switch } from "react-router"; +import useConnector from "components/hooks/services/useConnector"; import MainPageWithScroll from "components/MainPageWithScroll"; import PageTitle from "components/PageTitle"; import LoadingPage from "components/LoadingPage"; -import AccountSettings from "./components/AccountSettings"; import HeadTitle from "components/HeadTitle"; +import SideMenu from "components/SideMenu"; +import { Routes } from "pages/routes"; +import useRouter from "components/hooks/useRouterHook"; +import NotificationPage from "./pages/NotificationPage"; +import ConfigurationsPage from "./pages/ConfigurationsPage"; +import MetricsPage from "./pages/MetricsPage"; +import AccountPage from "./pages/AccountPage"; +import { DestinationsPage, SourcesPage } from "./pages/ConnectorsPage"; const Content = styled.div` margin: 0 33px 0 27px; height: 100%; + display: flex; + flex-direction: row; +`; +const MainView = styled.div` + width: 100%; + margin-left: 47px; `; const SettingsPage: React.FC = () => { + const { push, pathname } = useRouter(); + const { countNewSourceVersion, countNewDestinationVersion } = useConnector(); + + const menuItems = [ + { + id: `${Routes.Settings}${Routes.Account}`, + name: , + }, + { + id: `${Routes.Settings}${Routes.Source}`, + name: , + indicatorCount: countNewSourceVersion, + }, + { + id: `${Routes.Settings}${Routes.Destination}`, + name: , + indicatorCount: countNewDestinationVersion, + }, + { + id: `${Routes.Settings}${Routes.Configuration}`, + name: , + }, + { + id: `${Routes.Settings}${Routes.Notifications}`, + name: , + }, + { + id: `${Routes.Settings}${Routes.Metrics}`, + name: , + }, + ]; + + const onSelectMenuItem = (newPath: string) => push(newPath); + return ( } @@ -25,9 +74,38 @@ const SettingsPage: React.FC = () => { } > - }> - - + + + + }> + + + + + + + + + + + + + + + + + + + + + + + + ); diff --git a/airbyte-webapp/src/pages/SettingsPage/components/FeedbackBlock.tsx b/airbyte-webapp/src/pages/SettingsPage/components/FeedbackBlock.tsx new file mode 100644 index 000000000000..aa3448be615a --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/components/FeedbackBlock.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import styled from "styled-components"; + +import Spinner from "components/Spinner"; + +export type FeedbackBlockProps = { + isLoading?: boolean; + successMessage?: React.ReactNode; + errorMessage?: React.ReactNode; +}; + +const SuccessBlock = styled.div` + margin: -10px 10px; + color: ${({ theme }) => theme.successColor}; + font-size: 13px; + line-height: 16px; + display: inline-block; + vertical-align: middle; +`; + +const ErrorBlock = styled(SuccessBlock)` + color: ${({ theme }) => theme.dangerColor}; +`; + +const FeedbackBlock: React.FC = ({ + isLoading, + errorMessage, + successMessage, +}) => { + if (isLoading) { + return ( + + + + ); + } + + if (errorMessage) { + return {errorMessage}; + } + + if (successMessage) { + return {successMessage}; + } + + return null; +}; + +export default FeedbackBlock; diff --git a/airbyte-webapp/src/pages/SettingsPage/components/useWorkspaceEditor.tsx b/airbyte-webapp/src/pages/SettingsPage/components/useWorkspaceEditor.tsx new file mode 100644 index 000000000000..f93ee14fc774 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/components/useWorkspaceEditor.tsx @@ -0,0 +1,53 @@ +import React, { useState } from "react"; +import useWorkspace from "../../../components/hooks/services/useWorkspaceHook"; +import { FormattedMessage } from "react-intl"; +import { useAsyncFn } from "react-use"; + +const useWorkspaceEditor = (): { + updateData: (data: { + email?: string; + anonymousDataCollection: boolean; + news: boolean; + securityUpdates: boolean; + }) => Promise; + errorMessage: React.ReactNode; + successMessage: React.ReactNode; + loading?: boolean; +} => { + const { updatePreferences } = useWorkspace(); + const [errorMessage, setErrorMessage] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + + const [{ loading }, updateData] = useAsyncFn( + async (data: { + news: boolean; + securityUpdates: boolean; + anonymousDataCollection: boolean; + email?: string; + }) => { + setErrorMessage(null); + setSuccessMessage(null); + try { + await updatePreferences({ + email: data.email, + anonymousDataCollection: data.anonymousDataCollection, + news: data.news, + securityUpdates: data.securityUpdates, + }); + setSuccessMessage(); + } catch (e) { + setErrorMessage(); + } + }, + [setErrorMessage, setSuccessMessage] + ); + + return { + updateData, + errorMessage, + successMessage, + loading, + }; +}; + +export default useWorkspaceEditor; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccountPage/AccountPage.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccountPage/AccountPage.tsx new file mode 100644 index 000000000000..155b88641ba7 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccountPage/AccountPage.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { FormattedMessage } from "react-intl"; +import styled from "styled-components"; + +import { ContentCard } from "components"; +import useWorkspace from "components/hooks/services/useWorkspaceHook"; +import HeadTitle from "components/HeadTitle"; +import AccountForm from "./components/AccountForm"; +import useWorkspaceEditor from "../../components/useWorkspaceEditor"; + +const SettingsCard = styled(ContentCard)` + max-width: 638px; + width: 100%; + margin-top: 12px; + + &:first-child { + margin-top: 0; + } +`; + +const Content = styled.div` + padding: 27px 26px 15px; +`; + +const AccountPage: React.FC = () => { + const { workspace } = useWorkspace(); + + const { + errorMessage, + successMessage, + // loading, + updateData, + } = useWorkspaceEditor(); + + const onSubmit = async (data: { email: string }) => { + await updateData({ ...workspace, ...data }); + }; + + return ( + <> + + }> + + + + + + ); +}; + +export default AccountPage; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccountPage/components/AccountForm.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccountPage/components/AccountForm.tsx new file mode 100644 index 000000000000..db95d819945a --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccountPage/components/AccountForm.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; +import styled from "styled-components"; +import { Field, FieldProps, Form, Formik } from "formik"; +import * as yup from "yup"; + +import { LoadingButton } from "components"; +import { Row, Cell } from "components/SimpleTableComponents"; +import LabeledInput from "components/LabeledInput"; + +const InputRow = styled(Row)` + height: auto; + margin-bottom: 40px; +`; + +const ButtonCell = styled(Cell)` + &:last-child { + text-align: left; + } + padding-left: 11px; + height: 9px; +`; + +const EmailForm = styled(Form)` + position: relative; +`; + +const Success = styled.div` + font-size: 13px; + color: ${({ theme }) => theme.successColor}; + position: absolute; + bottom: -19px; +`; + +const Error = styled(Success)` + color: ${({ theme }) => theme.dangerColor}; +`; + +const accountValidationSchema = yup.object().shape({ + email: yup.string().email("form.email.error").required("form.empty.error"), +}); + +type AccountFormProps = { + email: string; + successMessage?: React.ReactNode; + errorMessage?: React.ReactNode; + onSubmit: (data: { email: string }) => void; +}; + +const AccountForm: React.FC = ({ + email, + onSubmit, + successMessage, + errorMessage, +}) => { + const formatMessage = useIntl().formatMessage; + + return ( + + {({ isSubmitting, dirty, values }) => ( + + + + + {({ field, meta }: FieldProps) => ( + + ) : ( + "" + ) + } + label={} + /> + )} + + + + + + + + + {!dirty && + (successMessage ? ( + {successMessage} + ) : errorMessage ? ( + {errorMessage} + ) : null)} + + )} + + ); +}; + +export default AccountForm; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccountPage/index.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccountPage/index.tsx new file mode 100644 index 000000000000..4bf14c2f6ca4 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccountPage/index.tsx @@ -0,0 +1,3 @@ +import AccountPage from "./AccountPage"; + +export default AccountPage; diff --git a/airbyte-webapp/src/pages/AdminPage/components/ConfigurationView.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConfigurationsPage/ConfigurationsPage.tsx similarity index 90% rename from airbyte-webapp/src/pages/AdminPage/components/ConfigurationView.tsx rename to airbyte-webapp/src/pages/SettingsPage/pages/ConfigurationsPage/ConfigurationsPage.tsx index 9198faf57654..51975e1cf07e 100644 --- a/airbyte-webapp/src/pages/AdminPage/components/ConfigurationView.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConfigurationsPage/ConfigurationsPage.tsx @@ -8,13 +8,12 @@ import ContentCard from "components/ContentCard"; import config from "config"; import Link from "components/Link"; import DeploymentService from "core/resources/DeploymentService"; -import ImportConfigurationModal from "./ImportConfigurationModal"; -import LogsContent from "./LogsContent"; +import ImportConfigurationModal from "./components/ImportConfigurationModal"; +import LogsContent from "./components/LogsContent"; import HeadTitle from "components/HeadTitle"; const Content = styled.div` max-width: 813px; - margin: 4px auto; `; const ControlContent = styled(ContentCard)` @@ -45,7 +44,7 @@ const Warning = styled.div` font-weight: bold; `; -const ConfigurationView: React.FC = () => { +const ConfigurationsPage: React.FC = () => { const [isModalOpen, setIsModalOpen] = useState(false); const [error, setError] = useState(null); @@ -85,9 +84,9 @@ const ConfigurationView: React.FC = () => { return ( - }> + }> @@ -109,7 +108,7 @@ const ConfigurationView: React.FC = () => { /> - + }> @@ -143,4 +142,4 @@ const ConfigurationView: React.FC = () => { ); }; -export default ConfigurationView; +export default ConfigurationsPage; diff --git a/airbyte-webapp/src/pages/AdminPage/components/ImportConfigurationModal.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConfigurationsPage/components/ImportConfigurationModal.tsx similarity index 100% rename from airbyte-webapp/src/pages/AdminPage/components/ImportConfigurationModal.tsx rename to airbyte-webapp/src/pages/SettingsPage/pages/ConfigurationsPage/components/ImportConfigurationModal.tsx diff --git a/airbyte-webapp/src/pages/AdminPage/components/LogsContent.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConfigurationsPage/components/LogsContent.tsx similarity index 100% rename from airbyte-webapp/src/pages/AdminPage/components/LogsContent.tsx rename to airbyte-webapp/src/pages/SettingsPage/pages/ConfigurationsPage/components/LogsContent.tsx diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/ConfigurationsPage/index.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConfigurationsPage/index.tsx new file mode 100644 index 000000000000..aeb38ed29a66 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConfigurationsPage/index.tsx @@ -0,0 +1,3 @@ +import ConfigurationsPage from "./ConfigurationsPage"; + +export default ConfigurationsPage; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/DestinationsPage.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/DestinationsPage.tsx new file mode 100644 index 000000000000..9f07cac1c901 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/DestinationsPage.tsx @@ -0,0 +1,104 @@ +import React, { useCallback, useMemo, useState } from "react"; +import { useIntl } from "react-intl"; +import { useFetcher, useResource } from "rest-hooks"; +import { useAsyncFn } from "react-use"; + +import config from "config"; +import DestinationDefinitionResource from "core/resources/DestinationDefinition"; +import { DestinationResource } from "core/resources/Destination"; +import { DestinationDefinition } from "core/resources/DestinationDefinition"; +import useConnector from "components/hooks/services/useConnector"; +import ConnectorsView from "./components/ConnectorsView"; + +const DestinationsPage: React.FC = () => { + const [isUpdateSuccess, setIsUpdateSuccess] = useState(false); + const formatMessage = useIntl().formatMessage; + const { destinationDefinitions } = useResource( + DestinationDefinitionResource.listShape(), + { + workspaceId: config.ui.workspaceId, + } + ); + const { destinations } = useResource(DestinationResource.listShape(), { + workspaceId: config.ui.workspaceId, + }); + + const [feedbackList, setFeedbackList] = useState>({}); + + const updateDestinationDefinition = useFetcher( + DestinationDefinitionResource.updateShape() + ); + + const { hasNewDestinationVersion } = useConnector(); + + const onUpdateVersion = useCallback( + async ({ id, version }: { id: string; version: string }) => { + try { + await updateDestinationDefinition( + {}, + { + destinationDefinitionId: id, + dockerImageTag: version, + } + ); + setFeedbackList({ ...feedbackList, [id]: "success" }); + } catch (e) { + const messageId = + e.status === 422 ? "form.imageCannotFound" : "form.someError"; + setFeedbackList({ + ...feedbackList, + [id]: formatMessage({ id: messageId }), + }); + } + }, + [feedbackList, formatMessage, updateDestinationDefinition] + ); + + const usedDestinationDefinitions = useMemo(() => { + const destinationDefinitionMap = new Map(); + destinations.forEach((destination) => { + const destinationDefinition = destinationDefinitions.find( + (destinationDefinition) => + destinationDefinition.destinationDefinitionId === + destination.destinationDefinitionId + ); + + if (destinationDefinition) { + destinationDefinitionMap.set( + destinationDefinition.destinationDefinitionId, + destinationDefinition + ); + } + }); + + return Array.from(destinationDefinitionMap.values()); + }, [destinations, destinationDefinitions]); + + const { updateAllDestinationVersions } = useConnector(); + + const [{ loading, error }, onUpdate] = useAsyncFn(async () => { + setIsUpdateSuccess(false); + await updateAllDestinationVersions(); + setIsUpdateSuccess(true); + setTimeout(() => { + setIsUpdateSuccess(false); + }, 2000); + }, [updateAllDestinationVersions]); + + return ( + + ); +}; + +export default DestinationsPage; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/SourcesPage.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/SourcesPage.tsx new file mode 100644 index 000000000000..81a122a08345 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/SourcesPage.tsx @@ -0,0 +1,98 @@ +import React, { useCallback, useMemo, useState } from "react"; +import { useIntl } from "react-intl"; +import { useFetcher, useResource } from "rest-hooks"; +import { useAsyncFn } from "react-use"; + +import config from "config"; +import SourceDefinitionResource, { + SourceDefinition, +} from "core/resources/SourceDefinition"; +import { SourceResource } from "core/resources/Source"; +import useConnector from "components/hooks/services/useConnector"; +import ConnectorsView from "./components/ConnectorsView"; + +const SourcesPage: React.FC = () => { + const [isUpdateSuccess, setIsUpdateSucces] = useState(false); + const formatMessage = useIntl().formatMessage; + const { sources } = useResource(SourceResource.listShape(), { + workspaceId: config.ui.workspaceId, + }); + const { sourceDefinitions } = useResource( + SourceDefinitionResource.listShape(), + { + workspaceId: config.ui.workspaceId, + } + ); + + const updateSourceDefinition = useFetcher( + SourceDefinitionResource.updateShape() + ); + + const { hasNewSourceVersion, updateAllSourceVersions } = useConnector(); + + const [feedbackList, setFeedbackList] = useState>({}); + const onUpdateVersion = useCallback( + async ({ id, version }: { id: string; version: string }) => { + try { + await updateSourceDefinition( + {}, + { + sourceDefinitionId: id, + dockerImageTag: version, + } + ); + setFeedbackList({ ...feedbackList, [id]: "success" }); + } catch (e) { + const messageId = + e.status === 422 ? "form.imageCannotFound" : "form.someError"; + setFeedbackList({ + ...feedbackList, + [id]: formatMessage({ id: messageId }), + }); + } + }, + [feedbackList, formatMessage, updateSourceDefinition] + ); + + const usedSourcesDefinitions = useMemo(() => { + const sourceDefinitionMap = new Map(); + sources.forEach((source) => { + const sourceDestination = sourceDefinitions.find( + (sourceDefinition) => + sourceDefinition.sourceDefinitionId === source.sourceDefinitionId + ); + + if (sourceDestination) { + sourceDefinitionMap.set(source?.sourceDefinitionId, sourceDestination); + } + }); + + return Array.from(sourceDefinitionMap.values()); + }, [sources, sourceDefinitions]); + + const [{ loading, error }, onUpdate] = useAsyncFn(async () => { + setIsUpdateSucces(false); + await updateAllSourceVersions(); + setIsUpdateSucces(true); + setTimeout(() => { + setIsUpdateSucces(false); + }, 2000); + }, [updateAllSourceVersions]); + + return ( + + ); +}; + +export default SourcesPage; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorCell.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorCell.tsx new file mode 100644 index 000000000000..d29a20871a18 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorCell.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import styled from "styled-components"; +import Indicator from "components/Indicator"; +import { getIcon } from "utils/imageUtils"; + +type IProps = { + connectorName: string; + img?: string; + hasUpdate?: boolean; +}; + +const Content = styled.div<{ enabled?: boolean }>` + display: flex; + align-items: center; + padding-left: 30px; + position: relative; + margin: -5px 0; + min-width: 290px; +`; + +const Image = styled.div` + height: 25px; + width: 17px; + margin-right: 9px; +`; + +const Notification = styled(Indicator)` + position: absolute; + left: 8px; +`; + +const ConnectorCell: React.FC = ({ connectorName, img, hasUpdate }) => { + return ( + + {hasUpdate && } + {getIcon(img)} + {connectorName} + + ); +}; + +export default ConnectorCell; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorsView.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorsView.tsx new file mode 100644 index 000000000000..bad731ee1aba --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorsView.tsx @@ -0,0 +1,167 @@ +import React from "react"; +import { FormattedMessage } from "react-intl"; +import { CellProps } from "react-table"; + +import Table from "components/Table"; +import ConnectorCell from "./ConnectorCell"; +import ImageCell from "./ImageCell"; +import VersionCell from "./VersionCell"; +import { Block, FormContentTitle, Title } from "./PageComponents"; +import { SourceDefinition } from "core/resources/SourceDefinition"; +import UpgradeAllButton from "./UpgradeAllButton"; +import CreateConnector from "./CreateConnector"; +import HeadTitle from "components/HeadTitle"; +import { DestinationDefinition } from "core/resources/DestinationDefinition"; + +type ConnectorsViewProps = { + type: "sources" | "destinations"; + isUpdateSuccess: boolean; + hasNewConnectorVersion?: boolean; + onUpdateVersion: ({ id, version }: { id: string; version: string }) => void; + usedConnectorsDefinitions: SourceDefinition[] | DestinationDefinition[]; + connectorsDefinitions: SourceDefinition[] | DestinationDefinition[]; + loading: boolean; + error?: Error; + onUpdate: () => void; + feedbackList: Record; +}; + +const ConnectorsView: React.FC = ({ + type, + onUpdateVersion, + feedbackList, + isUpdateSuccess, + hasNewConnectorVersion, + usedConnectorsDefinitions, + loading, + error, + onUpdate, + connectorsDefinitions, +}) => { + const columns = React.useMemo( + () => [ + { + Header: , + accessor: "name", + customWidth: 25, + Cell: ({ + cell, + row, + }: CellProps<{ + latestDockerImageTag: string; + dockerImageTag: string; + icon?: string; + }>) => ( + + ), + }, + { + Header: , + accessor: "dockerRepository", + customWidth: 36, + Cell: ({ cell, row }: CellProps<{ documentationUrl: string }>) => ( + + ), + }, + { + Header: , + accessor: "dockerImageTag", + customWidth: 10, + }, + { + Header: ( + + + + ), + accessor: "latestDockerImageTag", + collapse: true, + Cell: ({ + cell, + row, + }: CellProps<{ + sourceDefinitionId: string; + dockerImageTag: string; + }>) => ( + + ), + }, + ], + [feedbackList, onUpdateVersion] + ); + + return ( + <> + + {usedConnectorsDefinitions.length ? ( + + + <FormattedMessage + id={ + type === "sources" + ? "admin.manageSource" + : "admin.manageDestination" + } + /> + <div> + <CreateConnector type={type} /> + {(hasNewConnectorVersion || isUpdateSuccess) && ( + <UpgradeAllButton + isLoading={loading} + hasError={!!error && !loading} + hasSuccess={isUpdateSuccess} + onUpdate={onUpdate} + /> + )} + </div> + +
+ + ) : null} + + + + <FormattedMessage + id={ + type === "sources" + ? "admin.availableSource" + : "admin.availableDestinations" + } + /> + {(hasNewConnectorVersion || isUpdateSuccess) && + !usedConnectorsDefinitions.length && ( + <UpgradeAllButton + isLoading={loading} + hasError={!!error && !loading} + hasSuccess={isUpdateSuccess} + onUpdate={onUpdate} + /> + )} + +
+ + + ); +}; + +export default ConnectorsView; diff --git a/airbyte-webapp/src/pages/AdminPage/components/CreateConnector.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/CreateConnector.tsx similarity index 98% rename from airbyte-webapp/src/pages/AdminPage/components/CreateConnector.tsx rename to airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/CreateConnector.tsx index a2deee61b195..6d0dff9cc740 100644 --- a/airbyte-webapp/src/pages/AdminPage/components/CreateConnector.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/CreateConnector.tsx @@ -7,7 +7,7 @@ import CreateConnectorModal from "./CreateConnectorModal"; import SourceDefinitionResource from "core/resources/SourceDefinition"; import config from "config"; import useRouter from "components/hooks/useRouterHook"; -import { Routes } from "../../routes"; +import { Routes } from "pages/routes"; import DestinationDefinitionResource from "core/resources/DestinationDefinition"; type IProps = { diff --git a/airbyte-webapp/src/pages/AdminPage/components/CreateConnectorModal.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/CreateConnectorModal.tsx similarity index 100% rename from airbyte-webapp/src/pages/AdminPage/components/CreateConnectorModal.tsx rename to airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/CreateConnectorModal.tsx diff --git a/airbyte-webapp/src/pages/AdminPage/components/ImageCell.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ImageCell.tsx similarity index 100% rename from airbyte-webapp/src/pages/AdminPage/components/ImageCell.tsx rename to airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ImageCell.tsx diff --git a/airbyte-webapp/src/pages/AdminPage/components/PageComponents.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/PageComponents.tsx similarity index 100% rename from airbyte-webapp/src/pages/AdminPage/components/PageComponents.tsx rename to airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/PageComponents.tsx diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/UpgradeAllButton.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/UpgradeAllButton.tsx new file mode 100644 index 000000000000..267315602f5f --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/UpgradeAllButton.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { FormattedMessage } from "react-intl"; +import styled from "styled-components"; +import { faRedoAlt } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { LoadingButton } from "components"; + +const UpdateButton = styled(LoadingButton)` + margin: -6px 0; + min-width: 120px; +`; + +const TryArrow = styled(FontAwesomeIcon)` + margin: 0 10px -1px 0; + font-size: 14px; +`; + +const UpdateButtonContent = styled.div` + position: relative; + display: inline-block; + margin-left: 5px; +`; + +const ErrorBlock = styled.div` + color: ${({ theme }) => theme.dangerColor}; + font-size: 11px; + position: absolute; + font-weight: normal; + bottom: -17px; + line-height: 11px; + right: 0; + left: -46px; +`; + +type UpdateAllButtonProps = { + onUpdate: () => void; + isLoading: boolean; + hasError: boolean; + hasSuccess: boolean; +}; + +const UpgradeAllButton: React.FC = ({ + onUpdate, + isLoading, + hasError, + hasSuccess, +}) => { + return ( + + {hasError && ( + + + + )} + + {hasSuccess ? ( + + ) : ( + <> + + + + )} + + + ); +}; + +export default UpgradeAllButton; diff --git a/airbyte-webapp/src/pages/AdminPage/components/VersionCell.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/VersionCell.tsx similarity index 100% rename from airbyte-webapp/src/pages/AdminPage/components/VersionCell.tsx rename to airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/VersionCell.tsx diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/index.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/index.tsx new file mode 100644 index 000000000000..7ca619a0a450 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/index.tsx @@ -0,0 +1,4 @@ +import SourcesPage from "./SourcesPage"; +import DestinationsPage from "./DestinationsPage"; + +export { SourcesPage, DestinationsPage }; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/MetricsPage/MetricsPage.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/MetricsPage/MetricsPage.tsx new file mode 100644 index 000000000000..e3d38b2bd51e --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/MetricsPage/MetricsPage.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import { FormattedMessage } from "react-intl"; +import styled from "styled-components"; + +import { ContentCard } from "components"; +import useWorkspace from "components/hooks/services/useWorkspaceHook"; +import HeadTitle from "components/HeadTitle"; +import MetricsForm from "./components/MetricsForm"; +import useWorkspaceEditor from "../../components/useWorkspaceEditor"; + +const SettingsCard = styled(ContentCard)` + max-width: 638px; + width: 100%; + margin-top: 12px; + + &:first-child { + margin-top: 0; + } +`; + +const Content = styled.div` + padding: 27px 26px 15px; +`; + +const MetricsPage: React.FC = () => { + const { workspace } = useWorkspace(); + + const { + errorMessage, + successMessage, + loading, + updateData, + } = useWorkspaceEditor(); + + const onChange = async (data: { anonymousDataCollection: boolean }) => { + await updateData({ ...workspace, ...data }); + }; + + return ( + <> + + }> + + + + + + ); +}; + +export default MetricsPage; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/MetricsPage/components/MetricsForm.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/MetricsPage/components/MetricsForm.tsx new file mode 100644 index 000000000000..5e1daa08c9af --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/MetricsPage/components/MetricsForm.tsx @@ -0,0 +1,87 @@ +import React from "react"; +import styled from "styled-components"; +import { FormattedMessage } from "react-intl"; + +import Label from "components/Label"; +import LabeledToggle from "components/LabeledToggle"; +import config from "config"; +import FeedbackBlock from "../../../components/FeedbackBlock"; + +export type MetricsFormProps = { + onChange: (data: { anonymousDataCollection: boolean }) => void; + anonymousDataCollection?: boolean; + successMessage?: React.ReactNode; + errorMessage?: React.ReactNode; + isLoading?: boolean; +}; + +const FormItem = styled.div` + display: flex; + flex-direction: row; + align-items: center; + min-height: 33px; + margin-bottom: 10px; +`; + +const DocsLink = styled.a` + text-decoration: none; + color: ${({ theme }) => theme.primaryColor}; + cursor: pointer; +`; + +const Subtitle = styled(Label)` + padding-bottom: 9px; +`; + +const Text = styled.div` + font-style: normal; + font-weight: normal; + font-size: 13px; + line-height: 150%; + padding-bottom: 9px; +`; + +const MetricsForm: React.FC = ({ + onChange, + anonymousDataCollection, + successMessage, + errorMessage, + isLoading, +}) => { + return ( + <> + + + + + ( + + {docs} + + ), + }} + /> + + + } + onChange={(event) => { + onChange({ anonymousDataCollection: event.target.checked }); + }} + /> + + + + ); +}; + +export default MetricsForm; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/MetricsPage/index.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/MetricsPage/index.tsx new file mode 100644 index 000000000000..63be456e7dd0 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/MetricsPage/index.tsx @@ -0,0 +1,3 @@ +import MetricsPage from "./MetricsPage"; + +export default MetricsPage; diff --git a/airbyte-webapp/src/pages/SettingsPage/components/AccountSettings.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/NotificationPage.tsx similarity index 64% rename from airbyte-webapp/src/pages/SettingsPage/components/AccountSettings.tsx rename to airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/NotificationPage.tsx index 0a6d6660985c..69dca44e7c8e 100644 --- a/airbyte-webapp/src/pages/SettingsPage/components/AccountSettings.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/NotificationPage.tsx @@ -3,9 +3,11 @@ import { FormattedMessage } from "react-intl"; import styled from "styled-components"; import { ContentCard } from "components"; -import { PreferencesForm } from "views/Settings/PreferencesForm"; +import NotificationsForm from "./components/NotificationsForm"; import useWorkspace from "components/hooks/services/useWorkspaceHook"; -import WebHookForm from "./WebHookForm"; +import WebHookForm from "./components/WebHookForm"; +import HeadTitle from "components/HeadTitle"; +import useWorkspaceEditor from "../../components/useWorkspaceEditor"; const SettingsCard = styled(ContentCard)` max-width: 638px; @@ -21,15 +23,14 @@ const Content = styled.div` padding: 27px 26px 15px; `; -const AccountSettings: React.FC = () => { +const NotificationPage: React.FC = () => { + const { workspace, updateWebhook, testWebhook } = useWorkspace(); const { - workspace, - updatePreferences, - updateWebhook, - testWebhook, - } = useWorkspace(); - const [errorMessage, setErrorMessage] = useState(null); - const [successMessage, setSuccessMessage] = useState(null); + errorMessage, + successMessage, + loading, + updateData, + } = useWorkspaceEditor(); const [ errorWebhookMessage, setErrorWebhookMessage, @@ -39,20 +40,11 @@ const AccountSettings: React.FC = () => { setSuccessWebhookMessage, ] = useState(null); - const onSubmit = async (data: { - email: string; - anonymousDataCollection: boolean; + const onChange = async (data: { news: boolean; securityUpdates: boolean; }) => { - setErrorMessage(null); - setSuccessMessage(null); - try { - await updatePreferences(data); - setSuccessMessage(); - } catch (e) { - setErrorMessage(); - } + await updateData({ ...workspace, ...data }); }; const onSubmitWebhook = async (data: { webhook: string }) => { @@ -85,7 +77,12 @@ const AccountSettings: React.FC = () => { return ( <> - }> + + } + > { errorMessage={errorWebhookMessage} successMessage={successWebhookMessage} /> - - - }> - - { ); }; -export default AccountSettings; +export default NotificationPage; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/components/NotificationsForm.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/components/NotificationsForm.tsx new file mode 100644 index 000000000000..455fab63b0af --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/components/NotificationsForm.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import styled from "styled-components"; +import { FormattedMessage } from "react-intl"; + +import Label from "components/Label"; +import LabeledToggle from "components/LabeledToggle"; +import FeedbackBlock from "../../../components/FeedbackBlock"; + +export type NotificationsFormProps = { + onChange: (data: { news: boolean; securityUpdates: boolean }) => void; + preferencesValues: { + news: boolean; + securityUpdates: boolean; + }; + successMessage?: React.ReactNode; + errorMessage?: React.ReactNode; + isLoading?: boolean; +}; + +const FormItem = styled.div` + margin-bottom: 10px; +`; + +const Subtitle = styled(Label)` + padding-bottom: 9px; +`; + +const NotificationsForm: React.FC = ({ + onChange, + preferencesValues, + successMessage, + errorMessage, + isLoading, +}) => { + return ( + <> + + + + + + } + onChange={(event) => { + onChange({ + securityUpdates: event.target.checked, + news: preferencesValues.news, + }); + }} + /> + + + + } + onChange={(event) => { + onChange({ + news: event.target.checked, + securityUpdates: preferencesValues.securityUpdates, + }); + }} + /> + + + ); +}; + +export default NotificationsForm; diff --git a/airbyte-webapp/src/pages/SettingsPage/components/WebHookForm.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/components/WebHookForm.tsx similarity index 98% rename from airbyte-webapp/src/pages/SettingsPage/components/WebHookForm.tsx rename to airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/components/WebHookForm.tsx index 1707301344da..2b6791ecdd56 100644 --- a/airbyte-webapp/src/pages/SettingsPage/components/WebHookForm.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/components/WebHookForm.tsx @@ -17,11 +17,11 @@ const Text = styled.div` const InputRow = styled(Row)` height: auto; - margin-bottom: 28px; + margin-bottom: 40px; `; const Message = styled(Text)` - margin: -19px 0 0; + margin: -40px 0 21px; padding: 0; color: ${({ theme }) => theme.greyColor40}; `; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/index.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/index.tsx new file mode 100644 index 000000000000..7c47865305fb --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/NotificationPage/index.tsx @@ -0,0 +1,3 @@ +import NotificationPage from "./NotificationPage"; + +export default NotificationPage; diff --git a/airbyte-webapp/src/pages/AdminPage/components/SourcesView.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/SourcesPage.tsx similarity index 93% rename from airbyte-webapp/src/pages/AdminPage/components/SourcesView.tsx rename to airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/SourcesPage.tsx index bc225b29678d..fcbfe8c9fd55 100644 --- a/airbyte-webapp/src/pages/AdminPage/components/SourcesView.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/SourcesPage.tsx @@ -5,20 +5,20 @@ import { useFetcher, useResource } from "rest-hooks"; import { useAsyncFn } from "react-use"; import Table from "components/Table"; -import ConnectorCell from "./ConnectorCell"; -import ImageCell from "./ImageCell"; -import VersionCell from "./VersionCell"; +import ConnectorCell from "./components/ConnectorCell"; +import ImageCell from "./components/ImageCell"; +import VersionCell from "./components/VersionCell"; import config from "config"; -import { Block, FormContentTitle, Title } from "./PageComponents"; +import { Block, FormContentTitle, Title } from "./components/PageComponents"; import SourceDefinitionResource, { SourceDefinition, } from "core/resources/SourceDefinition"; import { SourceResource } from "core/resources/Source"; -import UpgradeAllButton from "./UpgradeAllButton"; +import UpgradeAllButton from "./components/UpgradeAllButton"; import useConnector from "components/hooks/services/useConnector"; import HeadTitle from "components/HeadTitle"; -const SourcesView: React.FC = () => { +const SourcesPage: React.FC = () => { const [successUpdate, setSuccessUpdate] = useState(false); const formatMessage = useIntl().formatMessage; const { sources } = useResource(SourceResource.listShape(), { @@ -192,4 +192,4 @@ const SourcesView: React.FC = () => { ); }; -export default SourcesView; +export default SourcesPage; diff --git a/airbyte-webapp/src/pages/AdminPage/components/ConnectorCell.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/ConnectorCell.tsx similarity index 100% rename from airbyte-webapp/src/pages/AdminPage/components/ConnectorCell.tsx rename to airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/ConnectorCell.tsx diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/ImageCell.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/ImageCell.tsx new file mode 100644 index 000000000000..a9cc0e5be690 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/ImageCell.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import styled from "styled-components"; + +type IProps = { + imageName: string; + link: string; +}; + +const Link = styled.a` + height: 17px; + margin-right: 9px; + color: ${({ theme }) => theme.darkPrimaryColor}; + + &:hover, + &:active { + color: ${({ theme }) => theme.primaryColor}; + } +`; + +const ImageCell: React.FC = ({ imageName, link }) => { + return ( + + {imageName} + + ); +}; + +export default ImageCell; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/PageComponents.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/PageComponents.tsx new file mode 100644 index 000000000000..171a9dc339e8 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/PageComponents.tsx @@ -0,0 +1,26 @@ +import styled from "styled-components"; +import { H5 } from "components"; + +const Title = styled(H5)` + color: ${({ theme }) => theme.darkPrimaryColor}; + margin-bottom: 19px; + display: flex; + justify-content: space-between; + align-items: center; +`; + +const Block = styled.div` + margin-bottom: 56px; +`; + +const FormContent = styled.div` + width: 253px; + margin: -10px 0 -10px 200px; + position: relative; +`; + +const FormContentTitle = styled(FormContent)` + margin: 0 0 0 200px; +`; + +export { Title, Block, FormContent, FormContentTitle }; diff --git a/airbyte-webapp/src/pages/AdminPage/components/UpgradeAllButton.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/UpgradeAllButton.tsx similarity index 100% rename from airbyte-webapp/src/pages/AdminPage/components/UpgradeAllButton.tsx rename to airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/UpgradeAllButton.tsx diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/VersionCell.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/VersionCell.tsx new file mode 100644 index 000000000000..b4401fded2d2 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/VersionCell.tsx @@ -0,0 +1,138 @@ +import React from "react"; +import { Formik, Form, FieldProps, Field } from "formik"; +import styled from "styled-components"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { Input, Button, Spinner } from "components"; +import { FormContent } from "./PageComponents"; + +type IProps = { + version: string; + currentVersion: string; + id: string; + onChange: ({ version, id }: { version: string; id: string }) => void; + feedback?: "success" | string; +}; + +const VersionInput = styled(Input)` + max-width: 145px; + margin-right: 19px; +`; + +const InputField = styled.div<{ showNote?: boolean }>` + display: inline-block; + position: relative; + background: ${({ theme }) => theme.whiteColor}; + + &:before { + position: absolute; + display: ${({ showNote }) => (showNote ? "block" : "none")}; + content: attr(data-before); + color: ${({ theme }) => theme.greyColor40}; + top: 10px; + right: 22px; + z-index: 3; + } + &:focus-within:before { + display: none; + } +`; + +const SuccessMessage = styled.div` + color: ${({ theme }) => theme.successColor}; + font-size: 12px; + line-height: 18px; + position: absolute; + text-align: right; + width: 205px; + left: -208px; + height: 100%; + display: flex; + align-items: center; + justify-content: flex-end; + white-space: break-spaces; +`; + +const ErrorMessage = styled(SuccessMessage)` + color: ${({ theme }) => theme.dangerColor}; + font-size: 11px; + line-height: 14px; +`; + +const VersionCell: React.FC = ({ + version, + id, + onChange, + feedback, + currentVersion, +}) => { + const formatMessage = useIntl().formatMessage; + + const renderFeedback = ( + dirty: boolean, + isSubmitting: boolean, + feedback?: string + ) => { + if (isSubmitting) { + return ( + + + + ); + } + + if (feedback && !dirty) { + if (feedback === "success") { + return ( + + + + ); + } else { + return {feedback}; + } + } + + return null; + }; + + return ( + + { + await onChange({ id, version: values.version }); + setSubmitting(false); + }} + > + {({ isSubmitting, dirty }) => ( +
+ {renderFeedback(dirty, isSubmitting, feedback)} + + {({ field }: FieldProps) => ( + + + + )} + + + + )} +
+
+ ); +}; + +export default VersionCell; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/index.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/index.tsx new file mode 100644 index 000000000000..a903a2946ea5 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/index.tsx @@ -0,0 +1,3 @@ +import SourcesPage from "./SourcesPage"; + +export default SourcesPage; diff --git a/airbyte-webapp/src/pages/routes.tsx b/airbyte-webapp/src/pages/routes.tsx index 8e814e140410..f1a13fbb07da 100644 --- a/airbyte-webapp/src/pages/routes.tsx +++ b/airbyte-webapp/src/pages/routes.tsx @@ -14,7 +14,6 @@ import DestinationPage from "./DestinationPage"; import PreferencesPage from "./PreferencesPage"; import OnboardingPage from "./OnboardingPage"; import ConnectionPage from "./ConnectionPage"; -import AdminPage from "./AdminPage"; import SettingsPage from "./SettingsPage"; import LoadingPage from "components/LoadingPage"; import MainView from "components/MainView"; @@ -39,8 +38,11 @@ export enum Routes { ConnectionNew = "/new-connection", SourceNew = "/new-source", DestinationNew = "/new-destination", - Admin = "/admin", Settings = "/settings", + Configuration = "/configuration", + Notifications = "/notifications", + Metrics = "/metrics", + Account = "/account", Root = "/", } @@ -78,11 +80,20 @@ const getPageName = (pathname: string) => { if (pathname.match(itemSourcePageRegex)) { return "Source Item Page"; } - if (pathname === Routes.Admin) { - return "Admin Page"; + if (pathname === `${Routes.Settings}${Routes.Source}`) { + return "Settings Sources Connectors Page"; } - if (pathname === Routes.Settings) { - return "Settings Page"; + if (pathname === `${Routes.Settings}${Routes.Destination}`) { + return "Settings Destinations Connectors Page"; + } + if (pathname === `${Routes.Settings}${Routes.Configuration}`) { + return "Settings Configuration Page"; + } + if (pathname === `${Routes.Settings}${Routes.Notifications}`) { + return "Settings Notifications Page"; + } + if (pathname === `${Routes.Settings}${Routes.Metrics}`) { + return "Settings Metrics Page"; } if (pathname === Routes.Connections) { return "Connections Page"; @@ -110,9 +121,6 @@ const MainViewRoutes = () => { - - - diff --git a/airbyte-webapp/src/views/layout/SideBar/SideBar.tsx b/airbyte-webapp/src/views/layout/SideBar/SideBar.tsx index 20587d9ade9e..af0ba816d6bf 100644 --- a/airbyte-webapp/src/views/layout/SideBar/SideBar.tsx +++ b/airbyte-webapp/src/views/layout/SideBar/SideBar.tsx @@ -1,12 +1,7 @@ import React from "react"; import styled from "styled-components"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faLifeRing, - faBook, - faCog, - faTools, -} from "@fortawesome/free-solid-svg-icons"; +import { faLifeRing, faBook, faCog } from "@fortawesome/free-solid-svg-icons"; import { faSlack } from "@fortawesome/free-brands-svg-icons"; import { FormattedMessage } from "react-intl"; import { NavLink } from "react-router-dom"; @@ -94,7 +89,7 @@ const HelpIcon = styled(FontAwesomeIcon)` line-height: 21px; `; -const AdminIcon = styled(FontAwesomeIcon)` +const SettingsIcon = styled(FontAwesomeIcon)` font-size: 16px; line-height: 15px; `; @@ -148,11 +143,17 @@ const SideBar: React.FC = () => {
  • - + + location.pathname.startsWith(Routes.Settings) + } + > {hasNewVersions ? : null} - + - +
  • @@ -184,14 +185,6 @@ const SideBar: React.FC = () => { -
  • - - - - - - -
  • {config.version ? (