From 880da0feaf2c551df641a68e7f3c25a32f1e3a60 Mon Sep 17 00:00:00 2001 From: Vijay Prasanna Date: Tue, 4 Apr 2023 12:50:14 +0530 Subject: [PATCH] feature (console): add component to list and manage database connections in the manage DB section PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7970 GitOrigin-RevId: 18e26e4ce660611c8f5dc3f440f156ec9311be13 --- .../Services/Data/Schema/ManageDatabase.tsx | 20 +- .../hooks/useCheckDatabaseLatency.tsx | 2 +- .../ListConnectedDatabases.stories.tsx | 10 +- .../ListConnectedDatabases.tsx | 259 +++++++++++++++++- .../ListConnectedDatabases/parts/Details.tsx | 85 ++++++ .../parts/InconsistentSourceDetails.tsx | 53 ++++ .../hooks/useDatabaseLatencyCheck.ts | 188 +++++++++++++ .../hooks/useDatabaseVersion.ts | 45 +++ .../hooks/useInconsistentSources.ts | 9 + .../hooks/useUpdateProjectRegion.ts | 20 ++ .../ConnectDBRedesign/mocks/handlers.mock.ts | 63 +++++ .../lib/features/ConnectDBRedesign/types.d.ts | 46 ++++ .../ControlPlane/generatedGraphQLTypes.ts | 2 +- .../src/lib/features/ControlPlane/queries.ts | 4 +- .../lib/features/DataSource/alloydb/index.ts | 15 +- .../lib/features/DataSource/citus/index.ts | 14 +- .../features/DataSource/cockroach/index.ts | 14 +- .../DataSource/common/defaultDatabaseProps.ts | 1 + .../src/lib/features/DataSource/index.ts | 14 + .../lib/features/DataSource/mssql/index.ts | 13 +- .../lib/features/DataSource/postgres/index.ts | 14 +- .../src/lib/features/DataSource/types.ts | 2 + .../ManageAgents/components/ManageAgents.tsx | 4 - .../lib/features/hasura-metadata-api/index.ts | 6 +- .../lib/features/hasura-metadata-api/types.ts | 12 + .../useInconsistentMetadata.ts | 46 ++++ 26 files changed, 928 insertions(+), 33 deletions(-) create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/parts/Details.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/parts/InconsistentSourceDetails.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useDatabaseLatencyCheck.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useDatabaseVersion.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useInconsistentSources.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useUpdateProjectRegion.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/types.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/useInconsistentMetadata.ts diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/Schema/ManageDatabase.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/Schema/ManageDatabase.tsx index d3338bba61a..200a21a8123 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/Schema/ManageDatabase.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/Schema/ManageDatabase.tsx @@ -59,6 +59,8 @@ import { useVPCBannerVisibility, } from './utils'; import { NeonDashboardLink } from '../DataSources/CreateDataSource/Neon/components/NeonDashboardLink'; +import { Collapsible } from '../../../../new-components/Collapsible'; +import { IconTooltip } from '../../../../new-components/Tooltip'; const KNOW_MORE_PROJECT_REGION_UPDATE = 'https://hasura.io/docs/latest/projects/regions/#changing-region-of-an-existing-project'; @@ -586,8 +588,22 @@ const ManageDatabase: React.FC = ({ ) : null} -
- +
+
+ + Data Connector Agents + +
+ } + > + +
diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDB/hooks/useCheckDatabaseLatency.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDB/hooks/useCheckDatabaseLatency.tsx index 54a73771b87..9ad7edaa8f4 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDB/hooks/useCheckDatabaseLatency.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDB/hooks/useCheckDatabaseLatency.tsx @@ -98,7 +98,7 @@ const useInsertIntoDBLatencyTable = () => { jobId: props.jobId, projectId: props.projectId, isLatencyDisplayed: true, - datasDifferenceInMilliseconds: props.dateDiff, + dateDifferenceInMilliseconds: props.dateDiff, }); }, retry: 1, diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/ListConnectedDatabases.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/ListConnectedDatabases.stories.tsx index be252cb1caf..a02db62632d 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/ListConnectedDatabases.stories.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/ListConnectedDatabases.stories.tsx @@ -1,17 +1,17 @@ import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query'; import { ComponentStory, ComponentMeta } from '@storybook/react'; import { handlers } from '../../mocks/handlers.mock'; - import { ListConnectedDatabases } from './ListConnectedDatabases'; export default { component: ListConnectedDatabases, decorators: [ReactQueryDecorator()], - parameters: { - msw: handlers(), - }, } as ComponentMeta; -export const Primary: ComponentStory = () => ( +export const Basic: ComponentStory = () => ( ); + +Basic.parameters = { + msw: handlers(), +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/ListConnectedDatabases.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/ListConnectedDatabases.tsx index 852533dbb79..9ced378b297 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/ListConnectedDatabases.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/ListConnectedDatabases.tsx @@ -1,39 +1,173 @@ import { CardedTable } from '../../../../new-components/CardedTable'; import { Button } from '../../../../new-components/Button'; -import { FaEdit, FaTrash, FaUndo } from 'react-icons/fa'; +import { + FaCheck, + FaEdit, + FaExclamationTriangle, + FaMinusCircle, + FaTrash, + FaUndo, + FaRedoAlt, + FaExternalLinkAlt, +} from 'react-icons/fa'; import { useMetadata } from '../../../MetadataAPI'; import _push from '../../../../components/Services/Data/push'; -import { useAppDispatch } from '../../../../storeHooks'; import { useReloadSource } from '../../hooks/useReloadSource'; import { useDropSource } from '../../hooks/useDropSource'; +import { getRoute } from '../../../../utils/getDataRoute'; +import { IndicatorCard } from '../../../../new-components/IndicatorCard'; +import Skeleton from 'react-loading-skeleton'; +import { useInconsistentSources } from '../../hooks/useInconsistentSources'; +import { Details } from './parts/Details'; +import { useState } from 'react'; +import { useDatabaseVersion } from '../../hooks/useDatabaseVersion'; +import { useDatabaseLatencyCheck } from '../../hooks/useDatabaseLatencyCheck'; +import { BiTimer } from 'react-icons/bi'; +import { hasuraToast } from '../../../../new-components/Toasts'; +import { Latency } from '../../types'; +import { Badge } from '../../../../new-components/Badge'; +import { LearnMoreLink } from '../../../../new-components/LearnMoreLink'; +import { getProjectId, isCloudConsole } from '../../../../utils/cloudConsole'; +import globals from '../../../../Globals'; +import { useUpdateProjectRegion } from '../../hooks/useUpdateProjectRegion'; +import { useAppDispatch } from '../../../../storeHooks'; + +const LatencyBadge = ({ + latencies, + dataSourceName, +}: { + latencies: Latency[]; + dataSourceName: string; +}) => { + const currentDataSourceLatencyInfo = latencies.find( + latencyInfo => latencyInfo.dataSourceName === dataSourceName + ); + + if (!currentDataSourceLatencyInfo) return null; + + if (currentDataSourceLatencyInfo.avgLatency < 100) + return ( + + Connection + + ); + + if (currentDataSourceLatencyInfo.avgLatency < 200) + return ( + + Acceptable + + ); + + return ( + + Elevated Latency + + ); +}; + +export const ListConnectedDatabases = (props?: { className?: string }) => { + const [showAccelerateProjectSection, setShowAccelerateProjectSection] = + useState(false); + + const { + data: { latencies, rowId } = {}, + refetch, + isLoading: databaseCheckLoading, + } = useDatabaseLatencyCheck({ + enabled: false, + onSuccess: data => { + console.log('on success', data); + const result = (data as any).latencies as Latency[]; + const isAnyLatencyHigh = result.find(latency => latency.avgLatency > 200); + setShowAccelerateProjectSection(!!isAnyLatencyHigh); + }, + onError: err => { + hasuraToast({ + type: 'error', + title: 'Could not fetch latency data!', + message: 'Something went wrong', + }); + }, + }); + + const { + mutate: updateProjectRegionForRowId, + // isLoading: isUpdatingProjectRegion, + } = useUpdateProjectRegion(); -export const ListConnectedDatabases = () => { const dispatch = useAppDispatch(); + const [activeRow, setActiveRow] = useState(); const { reloadSource, isLoading: isSourceReloading } = useReloadSource(); const { dropSource, isLoading: isSourceRemovalInProgress } = useDropSource(); + const { + data: inconsistentSources, + isLoading: isInconsistentFetchCallLoading, + } = useInconsistentSources(); - const { data: databaseList, isLoading } = useMetadata(m => + const { + data: databaseList, + isLoading, + isFetching, + } = useMetadata(m => m.metadata.sources.map(source => ({ dataSourceName: source.name, driver: source.kind, })) ); + const { data: databaseVersions, isLoading: isDatabaseVersionLoading } = + useDatabaseVersion( + (databaseList ?? []).map(d => d.dataSourceName), + !isFetching + ); + + const isCurrentRow = (rowIndex: number) => rowIndex === activeRow; + if (isLoading) return <>Loading...; - const columns = ['database', 'driver']; + const columns = ['database', 'driver', '', '']; - const rowData = (databaseList ?? []).map(databaseItem => [ - + const rowData = (databaseList ?? []).map((databaseItem, index) => [ + {databaseItem.dataSourceName} , databaseItem.driver, -
+ isDatabaseVersionLoading || isInconsistentFetchCallLoading ? ( + + ) : ( +
entry.dataSourceName === databaseItem.dataSourceName + )?.version ?? '', + }} + dataSourceName={databaseItem.dataSourceName} + /> + ), +
+ +
, +
{ + console.log('parent event captured'); + setActiveRow(index); + }} + >
, ]); + // console.log( + // 'loading: ', + // databaseCheckLoading, + // 'result: ', + // latencies, + // 'any error: ', + // error + // ); + + const openUpdateProjectRegionPage = (_rowId?: string) => { + if (!_rowId) { + hasuraToast({ + type: 'error', + title: 'Could not fetch row Id to update!', + message: 'Something went wrong', + }); + return; + } + + // update project region for the row Id + updateProjectRegionForRowId(_rowId); + + // redirect to the cloud "change region for project page" + + const projectId = getProjectId(globals); + if (!projectId) { + return; + } + const cloudDetailsPage = `${window.location.protocol}//${window.location.host}/project/${projectId}/details?open_update_region_drawer=true`; + + window.open(cloudDetailsPage, '_blank'); + }; + return ( -
- +
+ {rowData.length ? ( + + ) : ( + + You don't have any data sources connected, please connect one to + continue. + + )} + + {showAccelerateProjectSection ? ( +
+ +
+ + Databases marked with “Elevated Latency” indicate that it took + us over 200 ms for this Hasura project to communicate with your + database. These conditions generally happen when databases and + projects are in geographically distant regions. This can cause + API and subsequently application performance issues. We want + your GraphQL APIs to be lightning fast, therefore we + recommend that you either deploy your Hasura project in the same + region as your database or select a database instance + that's closer to where you've deployed Hasura. + + +
+ + +
+
+
+
+ ) : ( + isCloudConsole(globals) && ( + + ) + )}
); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/parts/Details.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/parts/Details.tsx new file mode 100644 index 00000000000..20694c41d08 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/parts/Details.tsx @@ -0,0 +1,85 @@ +import clsx from 'clsx'; +import { useState } from 'react'; +import { FaAngleDown, FaAngleUp, FaExclamationTriangle } from 'react-icons/fa'; +import { Feature } from '../../../../DataSource'; +import { InconsistentObject } from '../../../../hasura-metadata-api'; +import { InconsistentSourceDetails } from './InconsistentSourceDetails'; + +export const DisplayDetails = ({ + details, +}: { + details: { + version: string | Feature.NotImplemented; + }; +}) => { + const [isExpanded, setIsExpanded] = useState(false); + + const { version } = details; + + if (version === Feature.NotImplemented) return null; + + if (version) + return ( +
+
+ Version: + {version} +
+ +
setIsExpanded(!isExpanded)} + className="cursor-pointer font-semibold flex items-center gap-2" + > + {isExpanded ? ( + <> + + Less + + ) : ( + <> + + More + + )} +
+
+ ); + + return ( +
+ Could not fetch + version info. +
+ ); +}; + +export const Details = ({ + dataSourceName, + details, + inconsistentSources, +}: { + dataSourceName: string; + details: { + version: string | Feature.NotImplemented; + }; + inconsistentSources: InconsistentObject[]; +}) => { + const inconsistentSource = inconsistentSources.find( + source => source.definition === dataSourceName + ); + + if (inconsistentSource) + return ( + + ); + + return ; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/parts/InconsistentSourceDetails.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/parts/InconsistentSourceDetails.tsx new file mode 100644 index 00000000000..9941f8005f2 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ListConnectedDatabases/parts/InconsistentSourceDetails.tsx @@ -0,0 +1,53 @@ +import { useState } from 'react'; +import { FaAngleDown, FaAngleUp, FaExclamationTriangle } from 'react-icons/fa'; +import { InconsistentObject } from '../../../../hasura-metadata-api'; +import { IndicatorCard } from '../../../../../new-components/IndicatorCard'; + +export const InconsistentSourceDetails = ({ + inconsistentSource, +}: { + inconsistentSource: InconsistentObject; +}) => { + const [isExpanded, setIsExpanded] = useState(false); + + return ( +
+
+ {!isExpanded ? ( +
+ + Source is inconsistent +
+ ) : ( +
+ +
+                {JSON.stringify(inconsistentSource.message)}
+              
+
+
+ )} +
+ +
setIsExpanded(!isExpanded)} + className="cursor-pointer font-semibold flex items-center gap-2" + > + {isExpanded ? ( + <> + + Hide + + ) : ( + <> + + More + + )} +
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useDatabaseLatencyCheck.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useDatabaseLatencyCheck.ts new file mode 100644 index 00000000000..efe1db0fc3d --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useDatabaseLatencyCheck.ts @@ -0,0 +1,188 @@ +/** + * This process works as follows + * 1. The console submits it's "ProjectId" to lux. + * 2. Lux gives back a "JobId" in return. + * 3. At this point, the console starts a timer locally. Let's call this `start_time`0 + * 4. The console polls the "JobId" on lux until it's gets a response from lux. + * 5. Stop the timer and calculate now() - start_time => "Latency" + * 6. Save the "Latency" back to the server for keeping a record of it. + * 7. Return the "Latency" info to the hook's consumer. + */ + +import { useMutation, useQuery, UseQueryOptions } from 'react-query'; +import globals from '../../../Globals'; +import { getProjectId } from '../../../utils/cloudConsole'; +import { CheckDatabaseLatencyResponse } from '../../ConnectDB/hooks'; +import { + controlPlaneClient, + fetchDatabaseLatencyJobId, + fetchInfoFromJobId, + insertInfoIntoDBLatencyQuery, +} from '../../ControlPlane'; +import { LatencyActionResponse, LatencyJobResponse } from '../types'; + +const getJobIdFromLux = async () => { + const projectId = getProjectId(globals); + + if (!projectId) { + return undefined; + } + + return controlPlaneClient.query( + fetchDatabaseLatencyJobId, + { + project_id: projectId, + } + ); +}; + +async function poll( + fn: () => Promise, + fnCondition: (result: ReturnType) => boolean, + waitMs: number, + maxPollNumber = 50 +) { + let iterCount = 0; + let result = await fn(); + while (fnCondition(result) && iterCount < maxPollNumber) { + await wait(ms); + result = await fn(); + iterCount++; + } + return result; +} + +function wait(ms = 1000) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + +const getLatencyPingInfo = async (jobId: string) => { + const fn = () => + controlPlaneClient.query(fetchInfoFromJobId, { + id: jobId, + }); + + const fnCondition = (jobStatusResponse: LatencyJobResponse) => + jobStatusResponse.data.jobs_by_pk.status === 'running'; + + const finalResult = await poll(fn, fnCondition, 1000); + + return finalResult; +}; + +type DbLatencyMutationProps = { + dateDifferenceInMilliseconds: number; + projectId: string | undefined; + jobId: string; +}; + +type LatencyData = CheckDatabaseLatencyResponse['insertDbLatencyData']; +const useInsertIntoDBLatencyTable = () => { + return useMutation({ + mutationFn: async (props: DbLatencyMutationProps): Promise => + controlPlaneClient.query(insertInfoIntoDBLatencyQuery, { + isLatencyDisplayed: true, + ...props, + }), + retry: 1, + }); +}; + +type QueryOptions = Omit; + +export const useDatabaseLatencyCheck = (props: QueryOptions) => { + const insertDbLatencyMutation = useInsertIntoDBLatencyTable(); + + return useQuery({ + queryKey: ['database_latency_check'], + queryFn: async () => { + // only for testing + // return { + // latencies: [ + // { + // dataSourceName: 'sqlite_test', + // avgLatency: 150, + // connectionSource: 'env_var', + // error: '', + // }, + // { + // dataSourceName: 'chinook', + // avgLatency: 90, + // connectionSource: 'env_var', + // error: '', + // }, + // { + // dataSourceName: 'mssql1', + // avgLatency: 270, + // connectionSource: 'env_var', + // error: '', + // }, + // ], + // rowId: 'somerowId', + // }; + + // Get Job Id from lux + const resultFromLux = await getJobIdFromLux(); + + // Start timer + const startTime = new Date().getTime(); + + // const JobId + const jobId = resultFromLux?.data?.checkDBLatency?.db_latency_job_id; + + if (!jobId) { + throw Error('Job ID was not found'); + } + + // poll the job id + const latencyResponse = await getLatencyPingInfo(jobId); + + if (!latencyResponse?.data?.jobs_by_pk?.status) { + throw Error(`status for job ${jobId} not available`); + } + + if (latencyResponse.data.jobs_by_pk.status === 'failed') { + const failedTaskEvent = + latencyResponse?.data?.jobs_by_pk?.tasks?.[0]?.task_events?.find( + taskEvent => taskEvent.event_type === 'failure' + ); + throw Error(failedTaskEvent?.error); + } + + const taskEvent = + latencyResponse?.data?.jobs_by_pk?.tasks?.[0]?.task_events?.find( + taskEvent => taskEvent.event_type === 'success' + ); + + // Save this data back to lux + insertDbLatencyMutation.mutate({ + dateDifferenceInMilliseconds: new Date().getTime() - startTime, + projectId: getProjectId(globals), + jobId, + }); + + const latencies = Object.entries( + taskEvent?.public_event_data.sources ?? {} + ).map(([source, latencyInfo]) => { + return { + dataSourceName: source, + connectionSource: latencyInfo.connection_source, + avgLatency: latencyInfo.avg_latency, + error: latencyInfo.error, + }; + }); + + return { + latencies, + rowId: insertDbLatencyMutation.data?.data.insert_db_latency_one.id, + }; + }, + enabled: props.enabled, + onSuccess: data => { + props.onSuccess?.(data); + }, + onError: props.onError, + }); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useDatabaseVersion.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useDatabaseVersion.ts new file mode 100644 index 00000000000..28195af75cc --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useDatabaseVersion.ts @@ -0,0 +1,45 @@ +import { AxiosInstance } from 'axios'; +import { useQuery } from 'react-query'; +import { DataSource } from '../../DataSource'; +import { useHttpClient } from '../../Network'; + +const getDatabaseVersion = ({ + httpClient, + dataSourceName, +}: { + httpClient: AxiosInstance; + dataSourceName: string; +}) => { + return DataSource(httpClient).getDatabaseVersion(dataSourceName); +}; + +export const useDatabaseVersion = ( + dataSourceNames: string[], + enabled?: boolean +) => { + const httpClient = useHttpClient(); + + return useQuery({ + queryKey: ['dbVersion', ...dataSourceNames], + queryFn: async () => { + const result = dataSourceNames.map(async dataSourceName => { + try { + const version = await getDatabaseVersion({ + dataSourceName, + httpClient, + }); + return { + dataSourceName, + version, + }; + } catch (err) { + return { + dataSourceName, + }; + } + }); + return Promise.all(result); + }, + enabled: enabled, + }); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useInconsistentSources.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useInconsistentSources.ts new file mode 100644 index 00000000000..c29011e9f4e --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useInconsistentSources.ts @@ -0,0 +1,9 @@ +import { useInconsistentMetadata } from '../../hasura-metadata-api'; + +export const useInconsistentSources = () => { + return useInconsistentMetadata(m => { + return m.inconsistent_objects.filter( + inconsistentObject => inconsistentObject.type === 'source' + ); + }); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useUpdateProjectRegion.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useUpdateProjectRegion.ts new file mode 100644 index 00000000000..ceafcf0b2cd --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useUpdateProjectRegion.ts @@ -0,0 +1,20 @@ +import { useMutation } from 'react-query'; +import { + controlPlaneClient, + updateUserClickedChangeProjectRegion, +} from '../../ControlPlane'; + +export const useUpdateProjectRegion = () => { + return useMutation({ + mutationFn: async (rowId: string) => { + if (!rowId) { + return; + } + + return controlPlaneClient.query(updateUserClickedChangeProjectRegion, { + rowId, + isChangeRegionClicked: true, + }); + }, + }); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/handlers.mock.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/handlers.mock.ts index 72ce6dd0318..bf7fbadf044 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/handlers.mock.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/handlers.mock.ts @@ -204,6 +204,28 @@ export const handlers = () => [ if (requestBody.type === 'get_source_kind_capabilities') return res(ctx.json(mockCapabilitiesResponse)); + if (requestBody.type === 'get_inconsistent_metadata') + return res( + ctx.json({ + inconsistent_objects: [ + { + definition: 'bikes', + message: { + exception: { + message: + '[Microsoft][ODBC Driver 17 for SQL Server]Login timeout expired', + type: 'unsuccessful_return_code', + }, + }, + name: 'source bikes', + reason: 'Inconsistent object: mssql connection error', + type: 'source', + }, + ], + is_consistent: false, + }) + ); + return res(ctx.json({})); }), rest.get(`http://localhost:8080/v1alpha1/config`, (req, res, ctx) => { @@ -226,4 +248,45 @@ export const handlers = () => [ }) ); }), + rest.post('http://localhost:8080/v2/query', (req, res, ctx) => { + const requestBody = req.body as Record; + + if ( + requestBody.type === 'run_sql' && + JSON.stringify(requestBody.args) === + JSON.stringify({ sql: 'SELECT VERSION()', source: 'chinook' }) + ) + return res( + ctx.json({ + result_type: 'TuplesOk', + result: [ + ['version'], + [ + 'PostgreSQL 12.12 (Debian 12.12-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit', + ], + ], + }) + ); + + if ( + requestBody.type === 'mssql_run_sql' && + JSON.stringify(requestBody.args) === + JSON.stringify({ + sql: 'SELECT @@VERSION as version;', + source: 'mssql1', + }) + ) + return res( + ctx.json({ + result_type: 'TuplesOk', + result: [ + ['version'], + [ + 'Microsoft SQL Server 2008 (SP1) - 10.0.2531.0 (X64) Mar 29 2009 10:11:52 Copyright (c) 1988-2008 Microsoft Corporation Express Edition (64-bit) on Windows NT 6.1 (Build 7600: )', + ], + ], + }) + ); + return res(ctx.json({})); + }), ]; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/types.d.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/types.d.ts index 055c3ed297c..ac3018258b3 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/types.d.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/types.d.ts @@ -18,3 +18,49 @@ export type DatabaseKind = | 'cockroach'; export type EEState = 'active' | 'inactive' | 'expired'; + +export type LatencyActionResponse = { + data: { + checkDBLatency: { + db_latency_job_id: string; + }; + }; +}; + +export type TaskEvent = { + id: string; + event_type: 'success' | 'scheduled' | 'created' | 'running' | 'failure'; + public_event_data: { + sources: { + [sourceName: string]: { + connection_source: string; + avg_latency: number; + error: string; + }; + } | null; + }; + error: string; +}; + +export type Task = { + id: string; + name: string; + task_events: TaskEvent[]; +}; + +export type LatencyJobResponse = { + data: { + jobs_by_pk: { + id: string; + status: 'failed' | 'success' | 'running'; + tasks: Task[]; + }; + }; +}; + +export type Latency = { + dataSourceName: string; + connectionSource: string; + avgLatency: number; + error: string; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/generatedGraphQLTypes.ts b/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/generatedGraphQLTypes.ts index 6586f8efa6c..5c7dbb0a602 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/generatedGraphQLTypes.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/generatedGraphQLTypes.ts @@ -60437,7 +60437,7 @@ export type Unnamed_3_MutationVariables = Exact<{ jobId: Scalars['uuid']; projectId: Scalars['uuid']; isLatencyDisplayed: Scalars['Boolean']; - datasDifferenceInMilliseconds: Scalars['Int']; + dateDifferenceInMilliseconds: Scalars['Int']; }>; export type Unnamed_3_Mutation = { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/queries.ts b/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/queries.ts index a007555f41e..17042c4813b 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/queries.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/queries.ts @@ -143,13 +143,13 @@ mutation ( $jobId: uuid!, $projectId: uuid!, $isLatencyDisplayed: Boolean!, - $datasDifferenceInMilliseconds: Int! + $dateDifferenceInMilliseconds: Int! ) { insert_db_latency_one(object: { job_id: $jobId, is_latency_displayed: $isLatencyDisplayed, project_id: $projectId, - console_check_duration: $datasDifferenceInMilliseconds + console_check_duration: $dateDifferenceInMilliseconds }) { id } diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/alloydb/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/alloydb/index.ts index 4ce84a62198..2f0b7dae31a 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/alloydb/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/alloydb/index.ts @@ -1,5 +1,5 @@ import { Table } from '../../hasura-metadata-types'; -import { Database } from '..'; +import { Database, GetVersionProps } from '..'; import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; import { getDatabaseConfiguration, @@ -10,6 +10,7 @@ import { getSupportedOperators, } from '../postgres/introspection'; import { getTableRows } from '../postgres/query'; +import { runSQL } from '../api'; import { postgresCapabilities } from '../common/capabilities'; export type AlloyDbTable = { name: string; schema: string }; @@ -17,6 +18,18 @@ export type AlloyDbTable = { name: string; schema: string }; export const alloy: Database = { ...defaultDatabaseProps, introspection: { + getVersion: async ({ dataSourceName, httpClient }: GetVersionProps) => { + const result = await runSQL({ + source: { + name: dataSourceName, + kind: 'postgres', + }, + sql: `SELECT VERSION()`, + httpClient, + }); + console.log(result); + return result.result?.[1][0] ?? ''; + }, getDriverInfo: async () => ({ name: 'alloy', displayName: 'AlloyDB', diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/citus/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/citus/index.ts index e2e3cbb741d..5922c70f0d5 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/citus/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/citus/index.ts @@ -3,7 +3,7 @@ import { Database, Feature } from '..'; import { runSQL } from '../api'; import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; import { adaptIntrospectedTables } from '../common/utils'; -import { GetTrackableTablesProps } from '../types'; +import { GetTrackableTablesProps, GetVersionProps } from '../types'; import { getTableColumns, getFKRelationships, @@ -18,6 +18,18 @@ export type CitusTable = { name: string; schema: string }; export const citus: Database = { ...defaultDatabaseProps, introspection: { + getVersion: async ({ dataSourceName, httpClient }: GetVersionProps) => { + const result = await runSQL({ + source: { + name: dataSourceName, + kind: 'citus', + }, + sql: `SELECT VERSION()`, + httpClient, + }); + console.log(result); + return result.result?.[1][0] ?? ''; + }, getDriverInfo: async () => ({ name: 'citus', displayName: 'Citus', diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/cockroach/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/cockroach/index.ts index 4cf4d5d9cae..dfaefe0c15e 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/cockroach/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/cockroach/index.ts @@ -3,7 +3,7 @@ import { Database, Feature } from '..'; import { runSQL } from '../api'; import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; import { adaptIntrospectedTables } from '../common/utils'; -import { GetTrackableTablesProps } from '../types'; +import { GetTrackableTablesProps, GetVersionProps } from '../types'; import { getTableColumns, getFKRelationships, @@ -18,6 +18,18 @@ export type CockroachDBTable = { name: string; schema: string }; export const cockroach: Database = { ...defaultDatabaseProps, introspection: { + getVersion: async ({ dataSourceName, httpClient }: GetVersionProps) => { + const result = await runSQL({ + source: { + name: dataSourceName, + kind: 'cockroach', + }, + sql: `SELECT VERSION()`, + httpClient, + }); + console.log(result); + return result.result?.[1][0] ?? ''; + }, getDriverInfo: async () => ({ name: 'cockroach', displayName: 'CockroachDB', diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/defaultDatabaseProps.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/defaultDatabaseProps.ts index e198e48ec0f..264c18a6ae2 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/defaultDatabaseProps.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/defaultDatabaseProps.ts @@ -6,6 +6,7 @@ export const defaultDatabaseProps: Database = { getSupportedQueryTypes: async () => Feature.NotImplemented, }, introspection: { + getVersion: async () => Feature.NotImplemented, getDriverInfo: async () => Feature.NotImplemented, getDatabaseConfiguration: async () => Feature.NotImplemented, getDriverCapabilities: async () => Feature.NotImplemented, diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/index.ts index 8b469c1e53c..a7dbcaa7eb4 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/index.ts @@ -20,6 +20,7 @@ import type { GetTableRowsProps, GetTablesListAsTreeProps, GetTrackableTablesProps, + GetVersionProps, // Property, IntrospectedTable, Operator, @@ -27,6 +28,7 @@ import type { TableColumn, TableFkRelationships, TableRow, + Version, WhereClause, } from './types'; @@ -73,6 +75,9 @@ export const getDriver = (dataSource: Source) => { export type Database = { introspection?: { + getVersion?: ( + props: GetVersionProps + ) => Promise; getDriverInfo: () => Promise; getDatabaseConfiguration: ( httpClient: AxiosInstance, @@ -198,6 +203,15 @@ export const DataSource = (httpClient: AxiosInstance) => ({ getNativeDrivers: async () => { return nativeDrivers; }, + getDatabaseVersion: async ( + dataSourceName: string + ): Promise => { + const database = await getDatabaseMethods({ dataSourceName, httpClient }); + return ( + database.introspection?.getVersion?.({ dataSourceName, httpClient }) ?? + Feature.NotImplemented + ); + }, connectDB: { getConfigSchema: async (driver: string) => { const driverName = ( diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mssql/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mssql/index.ts index 945aeb09033..16ee2369a41 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mssql/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/mssql/index.ts @@ -1,5 +1,5 @@ import { Table } from '../../hasura-metadata-types'; -import { Database, Feature } from '..'; +import { Database, Feature, GetVersionProps } from '..'; import { NetworkArgs, runSQL } from '../api'; import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; import { adaptIntrospectedTables } from '../common/utils'; @@ -17,6 +17,17 @@ export type MssqlTable = { schema: string; name: string }; export const mssql: Database = { ...defaultDatabaseProps, introspection: { + getVersion: async ({ dataSourceName, httpClient }: GetVersionProps) => { + const result = await runSQL({ + source: { + name: dataSourceName, + kind: 'mssql', + }, + sql: `SELECT @@VERSION as version;`, + httpClient, + }); + return result.result?.[1][0] ?? ''; + }, getDriverInfo: async () => ({ name: 'mssql', displayName: 'MS SQL Server', diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/index.ts index 415ffbd402b..ee8d419e8bd 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/index.ts @@ -1,5 +1,5 @@ import { Table } from '../../hasura-metadata-types'; -import { Database, GetDefaultQueryRootProps } from '..'; +import { Database, GetDefaultQueryRootProps, GetVersionProps } from '..'; import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; import { getDatabaseConfiguration, @@ -10,6 +10,7 @@ import { getSupportedOperators, } from './introspection'; import { getTableRows } from './query'; +import { runSQL } from '../api'; import { postgresCapabilities } from '../common/capabilities'; export type PostgresTable = { name: string; schema: string }; @@ -17,6 +18,17 @@ export type PostgresTable = { name: string; schema: string }; export const postgres: Database = { ...defaultDatabaseProps, introspection: { + getVersion: async ({ dataSourceName, httpClient }: GetVersionProps) => { + const result = await runSQL({ + source: { + name: dataSourceName, + kind: 'postgres', + }, + sql: `SELECT VERSION()`, + httpClient, + }); + return result.result?.[1][0] ?? ''; + }, getDriverInfo: async () => ({ name: 'postgres', displayName: 'Postgres', diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts index 546a3570d11..96d2be39a9c 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts @@ -153,6 +153,8 @@ export type Operator = { }; export type GetSupportedOperatorsProps = NetworkArgs; +export type Version = string; +export type GetVersionProps = { dataSourceName: string } & NetworkArgs; export type InsertRowArgs = { dataSourceName: string; httpClient: NetworkArgs['httpClient']; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/components/ManageAgents.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/components/ManageAgents.tsx index c34fa6f4cfd..8fe7aaf41e6 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/components/ManageAgents.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/components/ManageAgents.tsx @@ -8,10 +8,6 @@ export const ManageAgents = () => { return (
-

- Data Connector Agents -

-
{showCreateAgentForm ? ( diff --git a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/index.ts index e189cc04e8d..b2994d263b2 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/index.ts @@ -1,7 +1,11 @@ import * as MetadataSelectors from './selectors'; import * as MetadataUtils from './utils'; - +export { + useInconsistentMetadata, + useInvalidateInconsistentMetadata, +} from './useInconsistentMetadata'; export { useMetadata, useInvalidateMetadata } from './useMetadata'; export { areTablesEqual } from './areTablesEqual'; export { MetadataSelectors }; export { MetadataUtils }; +export { InconsistentMetadata, InconsistentObject } from './types'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/types.ts b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/types.ts new file mode 100644 index 00000000000..f992fb9a92f --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/types.ts @@ -0,0 +1,12 @@ +export type InconsistentObject = { + definition: any; + reason: string; + type: string; + name: string; + message?: string; +}; + +export type InconsistentMetadata = { + inconsistent_objects: InconsistentObject[]; + is_consistent: boolean; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/useInconsistentMetadata.ts b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/useInconsistentMetadata.ts new file mode 100644 index 00000000000..c7252471dc1 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-api/useInconsistentMetadata.ts @@ -0,0 +1,46 @@ +import { useCallback } from 'react'; +import { useQuery, useQueryClient } from 'react-query'; +import { runMetadataQuery } from '../DataSource'; +import { useHttpClient } from '../Network'; +import { InconsistentMetadata } from './types'; + +export const DEFAULT_STALE_TIME = 5 * 60000; // 5 minutes as default stale time + +export const QUERY_KEY = 'inconsistent_objects'; + +export const useInvalidateInconsistentMetadata = () => { + const queryClient = useQueryClient(); + const invalidate = useCallback( + () => queryClient.invalidateQueries([QUERY_KEY]), + [queryClient] + ); + + return invalidate; +}; + +export const useInconsistentMetadata = ( + selector?: (m: InconsistentMetadata) => T, + staleTime: number = DEFAULT_STALE_TIME +) => { + const httpClient = useHttpClient(); + const invalidateInconsistentMetadata = useInvalidateInconsistentMetadata(); + + const queryReturn = useQuery({ + queryKey: [QUERY_KEY], + queryFn: async () => { + const result = (await runMetadataQuery({ + httpClient, + body: { type: 'get_inconsistent_metadata', args: {} }, + })) as InconsistentMetadata; + return result; + }, + staleTime: staleTime || DEFAULT_STALE_TIME, + refetchOnWindowFocus: false, + select: selector, + }); + + return { + ...queryReturn, + invalidateInconsistentMetadata, + }; +};