diff --git a/airbyte-webapp/package-lock.json b/airbyte-webapp/package-lock.json index 6c3e573e444a..15104c3896ea 100644 --- a/airbyte-webapp/package-lock.json +++ b/airbyte-webapp/package-lock.json @@ -3951,6 +3951,15 @@ "@babel/runtime": "^7.7.2" } }, + "@rest-hooks/test": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@rest-hooks/test/-/test-6.2.0.tgz", + "integrity": "sha512-WPrjFeLvsc+OJM1VNwr5NM4fc0EoDo/qmbTi7rb27c21EJI1IIENXj/4EWg1WRtjMmY77sbq6IJTxxADr4u2OQ==", + "dev": true, + "requires": { + "@testing-library/react-hooks": "~7.0.0" + } + }, "@rest-hooks/use-enhanced-reducer": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@rest-hooks/use-enhanced-reducer/-/use-enhanced-reducer-1.0.5.tgz", @@ -4619,6 +4628,30 @@ } } }, + "@testing-library/react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.1.tgz", + "integrity": "sha512-bpEQ2SHSBSzBmfJ437NmnP+oArQ7aVmmULiAp6Ag2rtyLBLPNFSMmgltUbFGmQOJdPWo4Ub31kpUC5T46zXNwQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@types/react": ">=16.9.0", + "@types/react-dom": ">=16.9.0", + "@types/react-test-renderer": ">=16.9.0", + "react-error-boundary": "^3.1.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", + "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, "@testing-library/user-event": { "version": "12.8.3", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.8.3.tgz", @@ -5062,6 +5095,15 @@ "@types/react": "*" } }, + "@types/react-test-renderer": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz", + "integrity": "sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-widgets": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@types/react-widgets/-/react-widgets-4.4.4.tgz", @@ -18816,6 +18858,26 @@ "prop-types": "^15.7.2" } }, + "react-error-boundary": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.3.tgz", + "integrity": "sha512-A+F9HHy9fvt9t8SNDlonq01prnU8AmkjvGKV4kk8seB9kU3xMEO8J/PQlLVmoOIDODl5U2kufSBs4vrWIqhsAA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", + "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, "react-error-overlay": { "version": "6.0.9", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", diff --git a/airbyte-webapp/package.json b/airbyte-webapp/package.json index 321f9c0d39bb..d9cb31abc60d 100644 --- a/airbyte-webapp/package.json +++ b/airbyte-webapp/package.json @@ -48,8 +48,10 @@ "yup": "^0.32.9" }, "devDependencies": { + "@rest-hooks/test": "^6.2.0", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", + "@testing-library/react-hooks": "^7.0.1", "@testing-library/user-event": "^12.1.10", "@types/flat": "^5.0.1", "@types/jest": "^24.0.0", diff --git a/airbyte-webapp/src/components/Table/Table.tsx b/airbyte-webapp/src/components/Table/Table.tsx index db257b19637f..2994986e8a02 100644 --- a/airbyte-webapp/src/components/Table/Table.tsx +++ b/airbyte-webapp/src/components/Table/Table.tsx @@ -1,6 +1,13 @@ -import React, { memo } from "react"; +import React, { memo, useMemo } from "react"; import styled from "styled-components"; -import { ColumnInstance, useTable, Column, Cell } from "react-table"; +import { + Cell, + Column, + ColumnInstance, + SortingRule, + useSortBy, + useTable, +} from "react-table"; type IHeaderProps = { headerHighlighted?: boolean; @@ -81,6 +88,8 @@ type IProps = { data: any[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any onClickRow?: (data: any) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sortBy?: Array>; }; const Table: React.FC = ({ @@ -88,17 +97,32 @@ const Table: React.FC = ({ data, onClickRow, erroredRows, + sortBy, }) => { + const [plugins, config] = useMemo(() => { + const pl = []; + const plConfig: Record = {}; + + if (sortBy) { + pl.push(useSortBy); + plConfig.initialState = { sortBy }; + } + return [pl, plConfig]; + }, [sortBy]); const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow, - } = useTable({ - columns, - data, - }); + } = useTable( + { + ...config, + columns, + data, + }, + ...plugins + ); return ( diff --git a/airbyte-webapp/src/components/hooks/services/useConnector.test.tsx b/airbyte-webapp/src/components/hooks/services/useConnector.test.tsx new file mode 100644 index 000000000000..e256e504ecf9 --- /dev/null +++ b/airbyte-webapp/src/components/hooks/services/useConnector.test.tsx @@ -0,0 +1,92 @@ +import { makeCacheProvider, makeRenderRestHook } from "@rest-hooks/test"; +import { act } from "@testing-library/react-hooks"; + +import { sourceDefinitionService } from "core/domain/connector/SourceDefinitionService"; +import { destinationDefinitionService } from "core/domain/connector/DestinationDefinitionService"; + +import useConnector from "./useConnector"; +import SourceDefinitionResource from "core/resources/SourceDefinition"; +import DestinationDefinitionResource from "core/resources/DestinationDefinition"; + +jest.mock("core/domain/connector/SourceDefinitionService"); +jest.mock("core/domain/connector/DestinationDefinitionService"); + +const renderRestHook = makeRenderRestHook(makeCacheProvider); +const results = [ + { + request: SourceDefinitionResource.listShape(), + params: { workspaceId: "5ae6b09b-fdec-41af-aaf7-7d94cfc33ef6" }, + result: { + sourceDefinitions: [ + { + sourceDefinitionId: "sid1", + latestDockerImageTag: "0.0.2", + dockerImageTag: "0.0.1", + }, + { + sourceDefinitionId: "sid2", + latestDockerImageTag: "", + dockerImageTag: "0.0.1", + }, + ], + }, + }, + { + request: DestinationDefinitionResource.listShape(), + params: { workspaceId: "5ae6b09b-fdec-41af-aaf7-7d94cfc33ef6" }, + result: { + destinationDefinitions: [ + { + destinationDefinitionId: "did1", + latestDockerImageTag: "0.0.2", + dockerImageTag: "0.0.1", + }, + { + destinationDefinitionId: "did2", + latestDockerImageTag: "", + dockerImageTag: "0.0.1", + }, + ], + }, + }, +]; + +test("should not call sourceDefinition.updateVersion for deprecated call", async () => { + const { result, waitForNextUpdate } = renderRestHook(() => useConnector(), { + results, + }); + + (sourceDefinitionService.update as jest.Mock).mockResolvedValue([]); + + act(() => { + result.current.updateAllSourceVersions(); + }); + + await waitForNextUpdate(); + + expect(sourceDefinitionService.update).toHaveBeenCalledTimes(1); + expect(sourceDefinitionService.update).toHaveBeenCalledWith({ + dockerImageTag: "0.0.2", + sourceDefinitionId: "sid1", + }); +}); + +test("should not call destinationDefinition.updateVersion for deprecated call", async () => { + const { result, waitForNextUpdate } = renderRestHook(() => useConnector(), { + results, + }); + + (destinationDefinitionService.update as jest.Mock).mockResolvedValue([]); + + act(() => { + result.current.updateAllDestinationVersions(); + }); + + await waitForNextUpdate(); + + expect(destinationDefinitionService.update).toHaveBeenCalledTimes(1); + expect(destinationDefinitionService.update).toHaveBeenCalledWith({ + dockerImageTag: "0.0.2", + destinationDefinitionId: "did1", + }); +}); diff --git a/airbyte-webapp/src/components/hooks/services/useConnector.tsx b/airbyte-webapp/src/components/hooks/services/useConnector.tsx index 5b60b4f9aa76..7e1e5642455d 100644 --- a/airbyte-webapp/src/components/hooks/services/useConnector.tsx +++ b/airbyte-webapp/src/components/hooks/services/useConnector.tsx @@ -2,8 +2,9 @@ import { useFetcher, useResource } from "rest-hooks"; import config from "config"; import { useMemo } from "react"; -import SourceDefinitionResource from "../../../core/resources/SourceDefinition"; -import DestinationDefinitionResource from "../../../core/resources/DestinationDefinition"; +import SourceDefinitionResource from "core/resources/SourceDefinition"; +import DestinationDefinitionResource from "core/resources/DestinationDefinition"; +import { Connector } from "core/domain/connector"; type ConnectorService = { hasNewVersions: boolean; @@ -37,52 +38,19 @@ const useConnector = (): ConnectorService => { DestinationDefinitionResource.updateShape() ); - const hasNewSourceVersion = useMemo( - () => - sourceDefinitions.some( - (source) => source.latestDockerImageTag !== source.dockerImageTag - ), + const newSourceDefinitions = useMemo( + () => sourceDefinitions.filter(Connector.hasNewerVersion), [sourceDefinitions] ); - const hasNewDestinationVersion = useMemo( - () => - destinationDefinitions.some( - (destination) => - destination.latestDockerImageTag !== destination.dockerImageTag - ), - [destinationDefinitions] - ); - - const hasNewVersions = useMemo( - () => hasNewSourceVersion || hasNewDestinationVersion, - [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, + const newDestinationDefinitions = useMemo( + () => destinationDefinitions.filter(Connector.hasNewerVersion), [destinationDefinitions] ); const updateAllSourceVersions = async () => { - const updateList = sourceDefinitions.filter( - (source) => source.latestDockerImageTag !== source.dockerImageTag - ); - await Promise.all( - updateList?.map((item) => + newSourceDefinitions?.map((item) => updateSourceDefinition( {}, { @@ -95,13 +63,8 @@ const useConnector = (): ConnectorService => { }; const updateAllDestinationVersions = async () => { - const updateList = destinationDefinitions.filter( - (destination) => - destination.latestDockerImageTag !== destination.dockerImageTag - ); - await Promise.all( - updateList?.map((item) => + newDestinationDefinitions?.map((item) => updateDestinationDefinition( {}, { @@ -113,14 +76,18 @@ const useConnector = (): ConnectorService => { ); }; + const hasNewSourceVersion = newSourceDefinitions.length > 0; + const hasNewDestinationVersion = newDestinationDefinitions.length > 0; + const hasNewVersions = hasNewSourceVersion || hasNewDestinationVersion; + return { hasNewVersions, hasNewSourceVersion, hasNewDestinationVersion, updateAllSourceVersions, updateAllDestinationVersions, - countNewSourceVersion, - countNewDestinationVersion, + countNewSourceVersion: newSourceDefinitions.length, + countNewDestinationVersion: newDestinationDefinitions.length, }; }; diff --git a/airbyte-webapp/src/components/index.tsx b/airbyte-webapp/src/components/index.tsx index a94cf6ba2922..a9172b79e57c 100644 --- a/airbyte-webapp/src/components/index.tsx +++ b/airbyte-webapp/src/components/index.tsx @@ -4,6 +4,7 @@ export * from "./Spinner"; export * from "./StatusIcon"; export * from "./Label"; export * from "./LabeledControl"; +export * from "./LabeledInput"; export * from "./LabeledToggle"; export * from "./Link"; export * from "./TextWithHTML"; diff --git a/airbyte-webapp/src/core/domain/connector/DestinationDefinitionService.ts b/airbyte-webapp/src/core/domain/connector/DestinationDefinitionService.ts new file mode 100644 index 000000000000..15e4d197de80 --- /dev/null +++ b/airbyte-webapp/src/core/domain/connector/DestinationDefinitionService.ts @@ -0,0 +1,14 @@ +import { AirbyteRequestService } from "core/request/AirbyteRequestService"; +import { DestinationDefinition } from "core/resources/DestinationDefinition"; + +class DestinationDefinitionService extends AirbyteRequestService { + get url() { + return "destination_definitions"; + } + + public update(body: DestinationDefinition): Promise { + return this.fetch(`${this.url}/update`, body) as any; + } +} + +export const destinationDefinitionService = new DestinationDefinitionService(); diff --git a/airbyte-webapp/src/core/domain/connector/SourceDefinitionService.ts b/airbyte-webapp/src/core/domain/connector/SourceDefinitionService.ts new file mode 100644 index 000000000000..c2accd0d7014 --- /dev/null +++ b/airbyte-webapp/src/core/domain/connector/SourceDefinitionService.ts @@ -0,0 +1,14 @@ +import { AirbyteRequestService } from "core/request/AirbyteRequestService"; +import { SourceDefinition } from "core/resources/SourceDefinition"; + +class SourceDefinitionService extends AirbyteRequestService { + get url() { + return "source_definitions"; + } + + public update(body: SourceDefinition): Promise { + return this.fetch(`${this.url}/update`, body) as any; + } +} + +export const sourceDefinitionService = new SourceDefinitionService(); diff --git a/airbyte-webapp/src/core/domain/connector/connector.ts b/airbyte-webapp/src/core/domain/connector/connector.ts new file mode 100644 index 000000000000..4f95e60f15f0 --- /dev/null +++ b/airbyte-webapp/src/core/domain/connector/connector.ts @@ -0,0 +1,28 @@ +import { SourceDefinition } from "core/resources/SourceDefinition"; +import { DestinationDefinition } from "core/resources/DestinationDefinition"; +import { isSourceDefinition } from "./source"; + +export type ConnectorDefinition = SourceDefinition | DestinationDefinition; + +export function isConnectorDeprecated(connector: ConnectorDefinition): boolean { + return !connector.latestDockerImageTag; +} + +export class Connector { + static id(connector: ConnectorDefinition): string { + return isSourceDefinition(connector) + ? connector.sourceDefinitionId + : connector.destinationDefinitionId; + } + + static isDeprecated(connector: ConnectorDefinition): boolean { + return !connector.latestDockerImageTag; + } + + static hasNewerVersion(connector: ConnectorDefinition): boolean { + return ( + !Connector.isDeprecated(connector) && + connector.latestDockerImageTag !== connector.dockerImageTag + ); + } +} diff --git a/airbyte-webapp/src/core/domain/connector/index.ts b/airbyte-webapp/src/core/domain/connector/index.ts new file mode 100644 index 000000000000..a3d850eb018c --- /dev/null +++ b/airbyte-webapp/src/core/domain/connector/index.ts @@ -0,0 +1 @@ +export * from "./connector"; diff --git a/airbyte-webapp/src/core/domain/connector/source.ts b/airbyte-webapp/src/core/domain/connector/source.ts index 077e3545118a..391a307746a2 100644 --- a/airbyte-webapp/src/core/domain/connector/source.ts +++ b/airbyte-webapp/src/core/domain/connector/source.ts @@ -1,8 +1,8 @@ import { SourceDefinition } from "core/resources/SourceDefinition"; -import { DestinationDefinition } from "core/resources/DestinationDefinition"; +import { ConnectorDefinition } from "./connector"; export function isSourceDefinition( - item: SourceDefinition | DestinationDefinition -): item is SourceDefinition { - return (item as SourceDefinition).sourceDefinitionId !== undefined; + connector: ConnectorDefinition +): connector is SourceDefinition { + return (connector as SourceDefinition).sourceDefinitionId !== undefined; } diff --git a/airbyte-webapp/src/core/resources/DestinationDefinition.ts b/airbyte-webapp/src/core/resources/DestinationDefinition.ts index 6cb2bcff3ab1..b284b9f32f6c 100644 --- a/airbyte-webapp/src/core/resources/DestinationDefinition.ts +++ b/airbyte-webapp/src/core/resources/DestinationDefinition.ts @@ -1,4 +1,7 @@ import { MutateShape, ReadShape, Resource, SchemaDetail } from "rest-hooks"; + +import { destinationDefinitionService } from "core/domain/connector/DestinationDefinitionService"; + import BaseResource from "./BaseResource"; export interface DestinationDefinition { @@ -84,6 +87,12 @@ export default class DestinationDefinitionResource ): MutateShape> { return { ...super.partialUpdateShape(), + fetch( + _: Readonly>, + body: DestinationDefinition + ): Promise { + return destinationDefinitionService.update(body); + }, schema: this, }; } diff --git a/airbyte-webapp/src/core/resources/SourceDefinition.ts b/airbyte-webapp/src/core/resources/SourceDefinition.ts index 772f5583c21a..175f1edd1d40 100644 --- a/airbyte-webapp/src/core/resources/SourceDefinition.ts +++ b/airbyte-webapp/src/core/resources/SourceDefinition.ts @@ -1,4 +1,5 @@ import { MutateShape, ReadShape, Resource, SchemaDetail } from "rest-hooks"; +import { sourceDefinitionService } from "core/domain/connector/SourceDefinitionService"; import BaseResource from "./BaseResource"; export interface SourceDefinition { @@ -36,16 +37,10 @@ export default class SourceDefinitionResource fetch: async ( params: Readonly> ): Promise<{ sourceDefinitions: SourceDefinition[] }> => { - const definition = await this.fetch( - "post", - `${this.url(params)}/list`, - params - ); - const latestDefinition = await this.fetch( - "post", - `${this.url(params)}/list_latest`, - params - ); + const [definition, latestDefinition] = await Promise.all([ + this.fetch("post", `${this.url(params)}/list`, params), + this.fetch("post", `${this.url(params)}/list_latest`, params), + ]); const result: SourceDefinition[] = definition.sourceDefinitions.map( (source: SourceDefinition) => { @@ -81,6 +76,12 @@ export default class SourceDefinitionResource ): MutateShape> { return { ...super.partialUpdateShape(), + fetch( + _: Readonly>, + body: SourceDefinition + ): Promise { + return sourceDefinitionService.update(body); + }, schema: this, }; } diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/SourcesPage.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/SourcesPage.tsx index 81a122a08345..1b6e985fc78f 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/SourcesPage.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/SourcesPage.tsx @@ -54,16 +54,16 @@ const SourcesPage: React.FC = () => { [feedbackList, formatMessage, updateSourceDefinition] ); - const usedSourcesDefinitions = useMemo(() => { + const usedSourcesDefinitions: SourceDefinition[] = useMemo(() => { const sourceDefinitionMap = new Map(); sources.forEach((source) => { - const sourceDestination = sourceDefinitions.find( + const sourceDefinition = sourceDefinitions.find( (sourceDefinition) => sourceDefinition.sourceDefinitionId === source.sourceDefinitionId ); - if (sourceDestination) { - sourceDefinitionMap.set(source?.sourceDefinitionId, sourceDestination); + if (sourceDefinition) { + sourceDefinitionMap.set(source.sourceDefinitionId, sourceDefinition); } }); @@ -82,15 +82,15 @@ const SourcesPage: React.FC = () => { return ( ); }; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorCell.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorCell.tsx index d29a20871a18..0952dc727a79 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorCell.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorCell.tsx @@ -1,5 +1,6 @@ import React from "react"; import styled from "styled-components"; + import Indicator from "components/Indicator"; import { getIcon } from "utils/imageUtils"; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorsView.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorsView.tsx index bad731ee1aba..a95a554915a5 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorsView.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/ConnectorsView.tsx @@ -12,20 +12,23 @@ import UpgradeAllButton from "./UpgradeAllButton"; import CreateConnector from "./CreateConnector"; import HeadTitle from "components/HeadTitle"; import { DestinationDefinition } from "core/resources/DestinationDefinition"; +import { Connector, ConnectorDefinition } from "core/domain/connector"; 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; + onUpdateVersion: ({ id, version }: { id: string; version: string }) => void; feedbackList: Record; }; +const defaultSorting = [{ id: "name" }]; + const ConnectorsView: React.FC = ({ type, onUpdateVersion, @@ -44,20 +47,11 @@ const ConnectorsView: React.FC = ({ Header: , accessor: "name", customWidth: 25, - Cell: ({ - cell, - row, - }: CellProps<{ - latestDockerImageTag: string; - dockerImageTag: string; - icon?: string; - }>) => ( + Cell: ({ cell, row }: CellProps) => ( ), }, @@ -65,7 +59,7 @@ const ConnectorsView: React.FC = ({ Header: , accessor: "dockerRepository", customWidth: 36, - Cell: ({ cell, row }: CellProps<{ documentationUrl: string }>) => ( + Cell: ({ cell, row }: CellProps) => ( = ({ ), accessor: "latestDockerImageTag", collapse: true, - Cell: ({ - cell, - row, - }: CellProps<{ - sourceDefinitionId: string; - dockerImageTag: string; - }>) => ( + Cell: ({ cell, row }: CellProps) => ( ), @@ -105,6 +93,22 @@ const ConnectorsView: React.FC = ({ [feedbackList, onUpdateVersion] ); + const renderHeaderControls = (section: "used" | "available") => + ((section === "used" && usedConnectorsDefinitions.length > 0) || + (section === "available" && usedConnectorsDefinitions.length === 0)) && ( +
+ + {(hasNewConnectorVersion || isUpdateSuccess) && ( + + )} +
+ ); + return ( <> = ({ { id: type === "sources" ? "admin.sources" : "admin.destinations" }, ]} /> - {usedConnectorsDefinitions.length ? ( + {usedConnectorsDefinitions.length > 0 && ( <FormattedMessage @@ -123,21 +127,15 @@ const ConnectorsView: React.FC<ConnectorsViewProps> = ({ : "admin.manageDestination" } /> - <div> - <CreateConnector type={type} /> - {(hasNewConnectorVersion || isUpdateSuccess) && ( - <UpgradeAllButton - isLoading={loading} - hasError={!!error && !loading} - hasSuccess={isUpdateSuccess} - onUpdate={onUpdate} - /> - )} - </div> + {renderHeaderControls("used")} - +
- ) : null} + )} @@ -148,17 +146,13 @@ const ConnectorsView: React.FC<ConnectorsViewProps> = ({ : "admin.availableDestinations" } /> - {(hasNewConnectorVersion || isUpdateSuccess) && - !usedConnectorsDefinitions.length && ( - <UpgradeAllButton - isLoading={loading} - hasError={!!error && !loading} - hasSuccess={isUpdateSuccess} - onUpdate={onUpdate} - /> - )} + {renderHeaderControls("available")} -
+
); diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/CreateConnector.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/CreateConnector.tsx index 6d0dff9cc740..37278b909d89 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/CreateConnector.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/CreateConnector.tsx @@ -2,14 +2,16 @@ import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useFetcher } from "rest-hooks"; +import config from "config"; + import { Button } from "components"; -import CreateConnectorModal from "./CreateConnectorModal"; import SourceDefinitionResource from "core/resources/SourceDefinition"; -import config from "config"; import useRouter from "components/hooks/useRouterHook"; import { Routes } from "pages/routes"; import DestinationDefinitionResource from "core/resources/DestinationDefinition"; +import CreateConnectorModal from "./CreateConnectorModal"; + type IProps = { type: string; }; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/CreateConnectorModal.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/CreateConnectorModal.tsx index 200d750cffa2..a430768cc513 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/CreateConnectorModal.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/CreateConnectorModal.tsx @@ -2,14 +2,11 @@ import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import styled from "styled-components"; import * as yup from "yup"; - -import Modal from "components/Modal"; -import { Button } from "components"; -import Link from "components/Link"; import { Field, FieldProps, Form, Formik } from "formik"; -import LabeledInput from "components/LabeledInput"; + import config from "config"; -import StatusIcon from "components/StatusIcon"; + +import { Button, LabeledInput, Link, Modal, StatusIcon } from "components"; export type IProps = { errorMessage?: string; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/VersionCell.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/VersionCell.tsx index b4401fded2d2..7393fcc35d4e 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/VersionCell.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/ConnectorsPage/components/VersionCell.tsx @@ -1,9 +1,9 @@ import React from "react"; -import { Formik, Form, FieldProps, Field } from "formik"; +import { Field, FieldProps, Form, Formik } from "formik"; import styled from "styled-components"; import { FormattedMessage, useIntl } from "react-intl"; -import { Input, Button, Spinner } from "components"; +import { Input, LoadingButton } from "components"; import { FormContent } from "./PageComponents"; type IProps = { @@ -33,6 +33,7 @@ const InputField = styled.div<{ showNote?: boolean }>` right: 22px; z-index: 3; } + &:focus-within:before { display: none; } @@ -60,27 +61,15 @@ const ErrorMessage = styled(SuccessMessage)` `; const VersionCell: React.FC = ({ - version, id, + version, onChange, feedback, currentVersion, }) => { const formatMessage = useIntl().formatMessage; - const renderFeedback = ( - dirty: boolean, - isSubmitting: boolean, - feedback?: string - ) => { - if (isSubmitting) { - return ( - - - - ); - } - + const renderFeedback = (dirty: boolean, feedback?: string) => { if (feedback && !dirty) { if (feedback === "success") { return ( @@ -102,14 +91,11 @@ const VersionCell: React.FC = ({ initialValues={{ version, }} - onSubmit={async (values, { setSubmitting }) => { - await onChange({ id, version: values.version }); - setSubmitting(false); - }} + onSubmit={(values) => onChange({ id, version: values.version })} > {({ isSubmitting, dirty }) => (
- {renderFeedback(dirty, isSubmitting, feedback)} + {renderFeedback(dirty, feedback)} {({ field }: FieldProps) => ( = ({ )} - + )} diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/SourcesPage.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/SourcesPage.tsx deleted file mode 100644 index fcbfe8c9fd55..000000000000 --- a/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/SourcesPage.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import React, { useCallback, useMemo, useState } from "react"; -import { FormattedMessage, useIntl } from "react-intl"; -import { CellProps } from "react-table"; -import { useFetcher, useResource } from "rest-hooks"; -import { useAsyncFn } from "react-use"; - -import Table from "components/Table"; -import ConnectorCell from "./components/ConnectorCell"; -import ImageCell from "./components/ImageCell"; -import VersionCell from "./components/VersionCell"; -import config from "config"; -import { Block, FormContentTitle, Title } from "./components/PageComponents"; -import SourceDefinitionResource, { - SourceDefinition, -} from "core/resources/SourceDefinition"; -import { SourceResource } from "core/resources/Source"; -import UpgradeAllButton from "./components/UpgradeAllButton"; -import useConnector from "components/hooks/services/useConnector"; -import HeadTitle from "components/HeadTitle"; - -const SourcesPage: React.FC = () => { - const [successUpdate, setSuccessUpdate] = 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 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] - ); - - 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 () => { - setSuccessUpdate(false); - await updateAllSourceVersions(); - setSuccessUpdate(true); - setTimeout(() => { - setSuccessUpdate(false); - }, 2000); - }, [updateAllSourceVersions]); - - return ( - <> - - {usedSourcesDefinitions.length ? ( - - - <FormattedMessage id="admin.manageSource" /> - {(hasNewSourceVersion || successUpdate) && ( - <UpgradeAllButton - isLoading={loading} - hasError={!!error && !loading} - hasSuccess={successUpdate} - onUpdate={onUpdate} - /> - )} - -
- - ) : null} - - - - <FormattedMessage id="admin.availableSource" /> - {(hasNewSourceVersion || successUpdate) && - !usedSourcesDefinitions.length && ( - <UpgradeAllButton - isLoading={loading} - hasError={!!error && !loading} - hasSuccess={successUpdate} - onUpdate={onUpdate} - /> - )} - -
- - - ); -}; - -export default SourcesPage; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/ConnectorCell.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/ConnectorCell.tsx deleted file mode 100644 index 38867f26734e..000000000000 --- a/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/ConnectorCell.tsx +++ /dev/null @@ -1,40 +0,0 @@ -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; -`; - -const Image = styled.div` - height: 17px; - 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/SourcesPage/components/ImageCell.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/ImageCell.tsx deleted file mode 100644 index a9cc0e5be690..000000000000 --- a/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/ImageCell.tsx +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 171a9dc339e8..000000000000 --- a/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/PageComponents.tsx +++ /dev/null @@ -1,26 +0,0 @@ -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/SettingsPage/pages/SourcesPage/components/UpgradeAllButton.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/UpgradeAllButton.tsx deleted file mode 100644 index d68c56e593b5..000000000000 --- a/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/UpgradeAllButton.tsx +++ /dev/null @@ -1,73 +0,0 @@ -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; -`; - -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/SettingsPage/pages/SourcesPage/components/VersionCell.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/VersionCell.tsx deleted file mode 100644 index b4401fded2d2..000000000000 --- a/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/components/VersionCell.tsx +++ /dev/null @@ -1,138 +0,0 @@ -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 deleted file mode 100644 index a903a2946ea5..000000000000 --- a/airbyte-webapp/src/pages/SettingsPage/pages/SourcesPage/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import SourcesPage from "./SourcesPage"; - -export default SourcesPage;