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
This commit is contained in:
Vijay Prasanna 2023-04-04 12:50:14 +05:30 committed by hasura-bot
parent f454a41c29
commit 880da0feaf
26 changed files with 928 additions and 33 deletions

View File

@ -59,6 +59,8 @@ import {
useVPCBannerVisibility, useVPCBannerVisibility,
} from './utils'; } from './utils';
import { NeonDashboardLink } from '../DataSources/CreateDataSource/Neon/components/NeonDashboardLink'; 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 = const KNOW_MORE_PROJECT_REGION_UPDATE =
'https://hasura.io/docs/latest/projects/regions/#changing-region-of-an-existing-project'; 'https://hasura.io/docs/latest/projects/regions/#changing-region-of-an-existing-project';
@ -586,8 +588,22 @@ const ManageDatabase: React.FC<ManageDatabaseProps> = ({
) : null} ) : null}
<NeonDashboardLink className="mt-lg" /> <NeonDashboardLink className="mt-lg" />
<div className="mt-lg"> <hr className="my-md" />
<div className="mt-4">
<Collapsible
triggerChildren={
<div className="flex font-bold items-center text-gray-600 text-lg">
Data Connector Agents
<IconTooltip
message={
'Data Connector Agents act as an intermediary abstraction between a data source and the Hasura GraphQL Engine.'
}
/>
</div>
}
>
<ManageAgents /> <ManageAgents />
</Collapsible>
</div> </div>
</div> </div>
</Analytics> </Analytics>

View File

@ -98,7 +98,7 @@ const useInsertIntoDBLatencyTable = () => {
jobId: props.jobId, jobId: props.jobId,
projectId: props.projectId, projectId: props.projectId,
isLatencyDisplayed: true, isLatencyDisplayed: true,
datasDifferenceInMilliseconds: props.dateDiff, dateDifferenceInMilliseconds: props.dateDiff,
}); });
}, },
retry: 1, retry: 1,

View File

@ -1,17 +1,17 @@
import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query'; import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import { handlers } from '../../mocks/handlers.mock'; import { handlers } from '../../mocks/handlers.mock';
import { ListConnectedDatabases } from './ListConnectedDatabases'; import { ListConnectedDatabases } from './ListConnectedDatabases';
export default { export default {
component: ListConnectedDatabases, component: ListConnectedDatabases,
decorators: [ReactQueryDecorator()], decorators: [ReactQueryDecorator()],
parameters: {
msw: handlers(),
},
} as ComponentMeta<typeof ListConnectedDatabases>; } as ComponentMeta<typeof ListConnectedDatabases>;
export const Primary: ComponentStory<typeof ListConnectedDatabases> = () => ( export const Basic: ComponentStory<typeof ListConnectedDatabases> = () => (
<ListConnectedDatabases /> <ListConnectedDatabases />
); );
Basic.parameters = {
msw: handlers(),
};

View File

@ -1,39 +1,173 @@
import { CardedTable } from '../../../../new-components/CardedTable'; import { CardedTable } from '../../../../new-components/CardedTable';
import { Button } from '../../../../new-components/Button'; 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 { useMetadata } from '../../../MetadataAPI';
import _push from '../../../../components/Services/Data/push'; import _push from '../../../../components/Services/Data/push';
import { useAppDispatch } from '../../../../storeHooks';
import { useReloadSource } from '../../hooks/useReloadSource'; import { useReloadSource } from '../../hooks/useReloadSource';
import { useDropSource } from '../../hooks/useDropSource'; 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 (
<Badge color="green">
<FaCheck className="mr-xs" /> Connection
</Badge>
);
if (currentDataSourceLatencyInfo.avgLatency < 200)
return (
<Badge color="yellow">
<FaMinusCircle className="mr-xs" /> Acceptable
</Badge>
);
return (
<Badge color="red">
<FaExclamationTriangle className="mr-xs" /> Elevated Latency
</Badge>
);
};
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 dispatch = useAppDispatch();
const [activeRow, setActiveRow] = useState<number>();
const { reloadSource, isLoading: isSourceReloading } = useReloadSource(); const { reloadSource, isLoading: isSourceReloading } = useReloadSource();
const { dropSource, isLoading: isSourceRemovalInProgress } = useDropSource(); 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 => ({ m.metadata.sources.map(source => ({
dataSourceName: source.name, dataSourceName: source.name,
driver: source.kind, 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...</>; if (isLoading) return <>Loading...</>;
const columns = ['database', 'driver']; const columns = ['database', 'driver', '', ''];
const rowData = (databaseList ?? []).map(databaseItem => [ const rowData = (databaseList ?? []).map((databaseItem, index) => [
<a href="!#" className="text-secondary"> <a
href={getRoute().database(databaseItem.dataSourceName)}
className="text-secondary"
>
{databaseItem.dataSourceName} {databaseItem.dataSourceName}
</a>, </a>,
databaseItem.driver, databaseItem.driver,
<div className="flex gap-4 justify-end px-4"> isDatabaseVersionLoading || isInconsistentFetchCallLoading ? (
<Skeleton />
) : (
<Details
inconsistentSources={inconsistentSources ?? []}
details={{
version:
(databaseVersions ?? []).find(
entry => entry.dataSourceName === databaseItem.dataSourceName
)?.version ?? '',
}}
dataSourceName={databaseItem.dataSourceName}
/>
),
<div className="flex justify-center">
<LatencyBadge
latencies={latencies ?? []}
dataSourceName={databaseItem.dataSourceName}
/>
</div>,
<div
className="flex gap-4 justify-end px-4"
onClick={e => {
console.log('parent event captured');
setActiveRow(index);
}}
>
<Button <Button
icon={<FaUndo />} icon={<FaUndo />}
size="sm" size="sm"
onClick={() => reloadSource(databaseItem.dataSourceName)} onClick={() => reloadSource(databaseItem.dataSourceName)}
isLoading={isSourceReloading} isLoading={isSourceReloading && isCurrentRow(index)}
loadingText="Reloading" loadingText="Reloading"
> >
Reload Reload
@ -58,7 +192,7 @@ export const ListConnectedDatabases = () => {
onClick={() => { onClick={() => {
dropSource(databaseItem.driver, databaseItem.dataSourceName); dropSource(databaseItem.driver, databaseItem.dataSourceName);
}} }}
isLoading={isSourceRemovalInProgress} isLoading={isSourceRemovalInProgress && isCurrentRow(index)}
loadingText="Deleting" loadingText="Deleting"
> >
Remove Remove
@ -66,9 +200,110 @@ export const ListConnectedDatabases = () => {
</div>, </div>,
]); ]);
// 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 ( return (
<div> <div className={props?.className}>
<CardedTable columns={[...columns, null]} data={rowData} showActionCell /> {rowData.length ? (
<CardedTable
columns={[...columns, null]}
data={rowData}
showActionCell
/>
) : (
<IndicatorCard headline="No databases connected">
You don't have any data sources connected, please connect one to
continue.
</IndicatorCard>
)}
{showAccelerateProjectSection ? (
<div className="mt-xs">
<IndicatorCard
status="negative"
headline="Accelerate your Hasura Project"
>
<div className="flex items-center flex-row">
<span>
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 <b>lightning fast</b>, therefore we
recommend that you either deploy your Hasura project in the same
region as your database or select a database instance
that&apos;s closer to where you&apos;ve deployed Hasura.
<LearnMoreLink href="https://hasura.io/docs/latest/projects/regions/#changing-region-of-an-existing-project" />
</span>
<div className="flex items-center flex-row ml-xs">
<Button
className="mr-xs"
onClick={() => {
refetch();
}}
isLoading={databaseCheckLoading}
loadingText="Measuring Latencies..."
icon={<FaRedoAlt />}
>
Re-check Database Latency
</Button>
<Button
className="mr-xs"
onClick={() => openUpdateProjectRegionPage(rowId)}
icon={<FaExternalLinkAlt />}
>
Update Project Region
</Button>
</div>
</div>
</IndicatorCard>
</div>
) : (
isCloudConsole(globals) && (
<Button
onClick={() => {
refetch();
}}
icon={<BiTimer />}
isLoading={databaseCheckLoading}
loadingText="Measuring Latencies"
>
Check latency
</Button>
)
)}
</div> </div>
); );
}; };

View File

@ -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 (
<div className="flex justify-start">
<div
className={clsx(
'max-w-xs',
isExpanded
? 'whitespace-pre-line'
: 'overflow-hidden text-ellipsis whitespace-nowrap'
)}
title={version}
>
<b>Version: </b>
{version}
</div>
<div
onClick={() => setIsExpanded(!isExpanded)}
className="cursor-pointer font-semibold flex items-center gap-2"
>
{isExpanded ? (
<>
<FaAngleUp />
Less
</>
) : (
<>
<FaAngleDown />
More
</>
)}
</div>
</div>
);
return (
<div className="flex gap-2 items-center">
<FaExclamationTriangle className="text-yellow-500" /> Could not fetch
version info.
</div>
);
};
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 (
<InconsistentSourceDetails inconsistentSource={inconsistentSource} />
);
return <DisplayDetails details={details} />;
};

View File

@ -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 (
<div className="flex justify-between">
<div className="max-w-xl">
{!isExpanded ? (
<div className="flex gap-2 items-center">
<FaExclamationTriangle className="text-red-500" />
Source is inconsistent
</div>
) : (
<div>
<IndicatorCard
status="negative"
headline={inconsistentSource.reason}
>
<pre className="whitespace-pre-line">
{JSON.stringify(inconsistentSource.message)}
</pre>
</IndicatorCard>
</div>
)}
</div>
<div
onClick={() => setIsExpanded(!isExpanded)}
className="cursor-pointer font-semibold flex items-center gap-2"
>
{isExpanded ? (
<>
<FaAngleUp />
Hide
</>
) : (
<>
<FaAngleDown />
More
</>
)}
</div>
</div>
);
};

View File

@ -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<LatencyActionResponse>(
fetchDatabaseLatencyJobId,
{
project_id: projectId,
}
);
};
async function poll<ReturnType>(
fn: () => Promise<ReturnType>,
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<LatencyJobResponse>(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<LatencyData> =>
controlPlaneClient.query<LatencyData>(insertInfoIntoDBLatencyQuery, {
isLatencyDisplayed: true,
...props,
}),
retry: 1,
});
};
type QueryOptions = Omit<UseQueryOptions, 'queryFn'>;
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,
});
};

View File

@ -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,
});
};

View File

@ -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'
);
});
};

View File

@ -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,
});
},
});
};

View File

@ -204,6 +204,28 @@ export const handlers = () => [
if (requestBody.type === 'get_source_kind_capabilities') if (requestBody.type === 'get_source_kind_capabilities')
return res(ctx.json(mockCapabilitiesResponse)); 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({})); return res(ctx.json({}));
}), }),
rest.get(`http://localhost:8080/v1alpha1/config`, (req, res, ctx) => { 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<string, any>;
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 <X64> (Build 7600: )',
],
],
})
);
return res(ctx.json({}));
}),
]; ];

View File

@ -18,3 +18,49 @@ export type DatabaseKind =
| 'cockroach'; | 'cockroach';
export type EEState = 'active' | 'inactive' | 'expired'; 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;
};

View File

@ -60437,7 +60437,7 @@ export type Unnamed_3_MutationVariables = Exact<{
jobId: Scalars['uuid']; jobId: Scalars['uuid'];
projectId: Scalars['uuid']; projectId: Scalars['uuid'];
isLatencyDisplayed: Scalars['Boolean']; isLatencyDisplayed: Scalars['Boolean'];
datasDifferenceInMilliseconds: Scalars['Int']; dateDifferenceInMilliseconds: Scalars['Int'];
}>; }>;
export type Unnamed_3_Mutation = { export type Unnamed_3_Mutation = {

View File

@ -143,13 +143,13 @@ mutation (
$jobId: uuid!, $jobId: uuid!,
$projectId: uuid!, $projectId: uuid!,
$isLatencyDisplayed: Boolean!, $isLatencyDisplayed: Boolean!,
$datasDifferenceInMilliseconds: Int! $dateDifferenceInMilliseconds: Int!
) { ) {
insert_db_latency_one(object: { insert_db_latency_one(object: {
job_id: $jobId, job_id: $jobId,
is_latency_displayed: $isLatencyDisplayed, is_latency_displayed: $isLatencyDisplayed,
project_id: $projectId, project_id: $projectId,
console_check_duration: $datasDifferenceInMilliseconds console_check_duration: $dateDifferenceInMilliseconds
}) { }) {
id id
} }

View File

@ -1,5 +1,5 @@
import { Table } from '../../hasura-metadata-types'; import { Table } from '../../hasura-metadata-types';
import { Database } from '..'; import { Database, GetVersionProps } from '..';
import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; import { defaultDatabaseProps } from '../common/defaultDatabaseProps';
import { import {
getDatabaseConfiguration, getDatabaseConfiguration,
@ -10,6 +10,7 @@ import {
getSupportedOperators, getSupportedOperators,
} from '../postgres/introspection'; } from '../postgres/introspection';
import { getTableRows } from '../postgres/query'; import { getTableRows } from '../postgres/query';
import { runSQL } from '../api';
import { postgresCapabilities } from '../common/capabilities'; import { postgresCapabilities } from '../common/capabilities';
export type AlloyDbTable = { name: string; schema: string }; export type AlloyDbTable = { name: string; schema: string };
@ -17,6 +18,18 @@ export type AlloyDbTable = { name: string; schema: string };
export const alloy: Database = { export const alloy: Database = {
...defaultDatabaseProps, ...defaultDatabaseProps,
introspection: { 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 () => ({ getDriverInfo: async () => ({
name: 'alloy', name: 'alloy',
displayName: 'AlloyDB', displayName: 'AlloyDB',

View File

@ -3,7 +3,7 @@ import { Database, Feature } from '..';
import { runSQL } from '../api'; import { runSQL } from '../api';
import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; import { defaultDatabaseProps } from '../common/defaultDatabaseProps';
import { adaptIntrospectedTables } from '../common/utils'; import { adaptIntrospectedTables } from '../common/utils';
import { GetTrackableTablesProps } from '../types'; import { GetTrackableTablesProps, GetVersionProps } from '../types';
import { import {
getTableColumns, getTableColumns,
getFKRelationships, getFKRelationships,
@ -18,6 +18,18 @@ export type CitusTable = { name: string; schema: string };
export const citus: Database = { export const citus: Database = {
...defaultDatabaseProps, ...defaultDatabaseProps,
introspection: { 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 () => ({ getDriverInfo: async () => ({
name: 'citus', name: 'citus',
displayName: 'Citus', displayName: 'Citus',

View File

@ -3,7 +3,7 @@ import { Database, Feature } from '..';
import { runSQL } from '../api'; import { runSQL } from '../api';
import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; import { defaultDatabaseProps } from '../common/defaultDatabaseProps';
import { adaptIntrospectedTables } from '../common/utils'; import { adaptIntrospectedTables } from '../common/utils';
import { GetTrackableTablesProps } from '../types'; import { GetTrackableTablesProps, GetVersionProps } from '../types';
import { import {
getTableColumns, getTableColumns,
getFKRelationships, getFKRelationships,
@ -18,6 +18,18 @@ export type CockroachDBTable = { name: string; schema: string };
export const cockroach: Database = { export const cockroach: Database = {
...defaultDatabaseProps, ...defaultDatabaseProps,
introspection: { 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 () => ({ getDriverInfo: async () => ({
name: 'cockroach', name: 'cockroach',
displayName: 'CockroachDB', displayName: 'CockroachDB',

View File

@ -6,6 +6,7 @@ export const defaultDatabaseProps: Database = {
getSupportedQueryTypes: async () => Feature.NotImplemented, getSupportedQueryTypes: async () => Feature.NotImplemented,
}, },
introspection: { introspection: {
getVersion: async () => Feature.NotImplemented,
getDriverInfo: async () => Feature.NotImplemented, getDriverInfo: async () => Feature.NotImplemented,
getDatabaseConfiguration: async () => Feature.NotImplemented, getDatabaseConfiguration: async () => Feature.NotImplemented,
getDriverCapabilities: async () => Feature.NotImplemented, getDriverCapabilities: async () => Feature.NotImplemented,

View File

@ -20,6 +20,7 @@ import type {
GetTableRowsProps, GetTableRowsProps,
GetTablesListAsTreeProps, GetTablesListAsTreeProps,
GetTrackableTablesProps, GetTrackableTablesProps,
GetVersionProps,
// Property, // Property,
IntrospectedTable, IntrospectedTable,
Operator, Operator,
@ -27,6 +28,7 @@ import type {
TableColumn, TableColumn,
TableFkRelationships, TableFkRelationships,
TableRow, TableRow,
Version,
WhereClause, WhereClause,
} from './types'; } from './types';
@ -73,6 +75,9 @@ export const getDriver = (dataSource: Source) => {
export type Database = { export type Database = {
introspection?: { introspection?: {
getVersion?: (
props: GetVersionProps
) => Promise<Version | Feature.NotImplemented>;
getDriverInfo: () => Promise<DriverInfoResponse | Feature.NotImplemented>; getDriverInfo: () => Promise<DriverInfoResponse | Feature.NotImplemented>;
getDatabaseConfiguration: ( getDatabaseConfiguration: (
httpClient: AxiosInstance, httpClient: AxiosInstance,
@ -198,6 +203,15 @@ export const DataSource = (httpClient: AxiosInstance) => ({
getNativeDrivers: async () => { getNativeDrivers: async () => {
return nativeDrivers; return nativeDrivers;
}, },
getDatabaseVersion: async (
dataSourceName: string
): Promise<string | Feature.NotImplemented> => {
const database = await getDatabaseMethods({ dataSourceName, httpClient });
return (
database.introspection?.getVersion?.({ dataSourceName, httpClient }) ??
Feature.NotImplemented
);
},
connectDB: { connectDB: {
getConfigSchema: async (driver: string) => { getConfigSchema: async (driver: string) => {
const driverName = ( const driverName = (

View File

@ -1,5 +1,5 @@
import { Table } from '../../hasura-metadata-types'; import { Table } from '../../hasura-metadata-types';
import { Database, Feature } from '..'; import { Database, Feature, GetVersionProps } from '..';
import { NetworkArgs, runSQL } from '../api'; import { NetworkArgs, runSQL } from '../api';
import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; import { defaultDatabaseProps } from '../common/defaultDatabaseProps';
import { adaptIntrospectedTables } from '../common/utils'; import { adaptIntrospectedTables } from '../common/utils';
@ -17,6 +17,17 @@ export type MssqlTable = { schema: string; name: string };
export const mssql: Database = { export const mssql: Database = {
...defaultDatabaseProps, ...defaultDatabaseProps,
introspection: { 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 () => ({ getDriverInfo: async () => ({
name: 'mssql', name: 'mssql',
displayName: 'MS SQL Server', displayName: 'MS SQL Server',

View File

@ -1,5 +1,5 @@
import { Table } from '../../hasura-metadata-types'; import { Table } from '../../hasura-metadata-types';
import { Database, GetDefaultQueryRootProps } from '..'; import { Database, GetDefaultQueryRootProps, GetVersionProps } from '..';
import { defaultDatabaseProps } from '../common/defaultDatabaseProps'; import { defaultDatabaseProps } from '../common/defaultDatabaseProps';
import { import {
getDatabaseConfiguration, getDatabaseConfiguration,
@ -10,6 +10,7 @@ import {
getSupportedOperators, getSupportedOperators,
} from './introspection'; } from './introspection';
import { getTableRows } from './query'; import { getTableRows } from './query';
import { runSQL } from '../api';
import { postgresCapabilities } from '../common/capabilities'; import { postgresCapabilities } from '../common/capabilities';
export type PostgresTable = { name: string; schema: string }; export type PostgresTable = { name: string; schema: string };
@ -17,6 +18,17 @@ export type PostgresTable = { name: string; schema: string };
export const postgres: Database = { export const postgres: Database = {
...defaultDatabaseProps, ...defaultDatabaseProps,
introspection: { 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 () => ({ getDriverInfo: async () => ({
name: 'postgres', name: 'postgres',
displayName: 'Postgres', displayName: 'Postgres',

View File

@ -153,6 +153,8 @@ export type Operator = {
}; };
export type GetSupportedOperatorsProps = NetworkArgs; export type GetSupportedOperatorsProps = NetworkArgs;
export type Version = string;
export type GetVersionProps = { dataSourceName: string } & NetworkArgs;
export type InsertRowArgs = { export type InsertRowArgs = {
dataSourceName: string; dataSourceName: string;
httpClient: NetworkArgs['httpClient']; httpClient: NetworkArgs['httpClient'];

View File

@ -8,10 +8,6 @@ export const ManageAgents = () => {
return ( return (
<div> <div>
<p className="text-xl text-gray-600 py-3 font-bold">
Data Connector Agents
</p>
<hr className="m-0" />
<ManageAgentsTable /> <ManageAgentsTable />
<Button onClick={() => setShowCreateAgentForm(true)}>Add Agent</Button> <Button onClick={() => setShowCreateAgentForm(true)}>Add Agent</Button>
{showCreateAgentForm ? ( {showCreateAgentForm ? (

View File

@ -1,7 +1,11 @@
import * as MetadataSelectors from './selectors'; import * as MetadataSelectors from './selectors';
import * as MetadataUtils from './utils'; import * as MetadataUtils from './utils';
export {
useInconsistentMetadata,
useInvalidateInconsistentMetadata,
} from './useInconsistentMetadata';
export { useMetadata, useInvalidateMetadata } from './useMetadata'; export { useMetadata, useInvalidateMetadata } from './useMetadata';
export { areTablesEqual } from './areTablesEqual'; export { areTablesEqual } from './areTablesEqual';
export { MetadataSelectors }; export { MetadataSelectors };
export { MetadataUtils }; export { MetadataUtils };
export { InconsistentMetadata, InconsistentObject } from './types';

View File

@ -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;
};

View File

@ -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 = <T = InconsistentMetadata>(
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,
};
};