mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
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:
parent
f454a41c29
commit
880da0feaf
@ -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<ManageDatabaseProps> = ({
|
||||
) : null}
|
||||
<NeonDashboardLink className="mt-lg" />
|
||||
|
||||
<div className="mt-lg">
|
||||
<ManageAgents />
|
||||
<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 />
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
</Analytics>
|
||||
|
@ -98,7 +98,7 @@ const useInsertIntoDBLatencyTable = () => {
|
||||
jobId: props.jobId,
|
||||
projectId: props.projectId,
|
||||
isLatencyDisplayed: true,
|
||||
datasDifferenceInMilliseconds: props.dateDiff,
|
||||
dateDifferenceInMilliseconds: props.dateDiff,
|
||||
});
|
||||
},
|
||||
retry: 1,
|
||||
|
@ -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<typeof ListConnectedDatabases>;
|
||||
|
||||
export const Primary: ComponentStory<typeof ListConnectedDatabases> = () => (
|
||||
export const Basic: ComponentStory<typeof ListConnectedDatabases> = () => (
|
||||
<ListConnectedDatabases />
|
||||
);
|
||||
|
||||
Basic.parameters = {
|
||||
msw: handlers(),
|
||||
};
|
||||
|
@ -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 (
|
||||
<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 [activeRow, setActiveRow] = useState<number>();
|
||||
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 => [
|
||||
<a href="!#" className="text-secondary">
|
||||
const rowData = (databaseList ?? []).map((databaseItem, index) => [
|
||||
<a
|
||||
href={getRoute().database(databaseItem.dataSourceName)}
|
||||
className="text-secondary"
|
||||
>
|
||||
{databaseItem.dataSourceName}
|
||||
</a>,
|
||||
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
|
||||
icon={<FaUndo />}
|
||||
size="sm"
|
||||
onClick={() => reloadSource(databaseItem.dataSourceName)}
|
||||
isLoading={isSourceReloading}
|
||||
isLoading={isSourceReloading && isCurrentRow(index)}
|
||||
loadingText="Reloading"
|
||||
>
|
||||
Reload
|
||||
@ -58,7 +192,7 @@ export const ListConnectedDatabases = () => {
|
||||
onClick={() => {
|
||||
dropSource(databaseItem.driver, databaseItem.dataSourceName);
|
||||
}}
|
||||
isLoading={isSourceRemovalInProgress}
|
||||
isLoading={isSourceRemovalInProgress && isCurrentRow(index)}
|
||||
loadingText="Deleting"
|
||||
>
|
||||
Remove
|
||||
@ -66,9 +200,110 @@ export const ListConnectedDatabases = () => {
|
||||
</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 (
|
||||
<div>
|
||||
<CardedTable columns={[...columns, null]} data={rowData} showActionCell />
|
||||
<div className={props?.className}>
|
||||
{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's closer to where you'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>
|
||||
);
|
||||
};
|
||||
|
@ -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} />;
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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,
|
||||
});
|
||||
};
|
@ -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,
|
||||
});
|
||||
};
|
@ -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'
|
||||
);
|
||||
});
|
||||
};
|
@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
@ -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<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({}));
|
||||
}),
|
||||
];
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -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<Version | Feature.NotImplemented>;
|
||||
getDriverInfo: () => Promise<DriverInfoResponse | Feature.NotImplemented>;
|
||||
getDatabaseConfiguration: (
|
||||
httpClient: AxiosInstance,
|
||||
@ -198,6 +203,15 @@ export const DataSource = (httpClient: AxiosInstance) => ({
|
||||
getNativeDrivers: async () => {
|
||||
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: {
|
||||
getConfigSchema: async (driver: string) => {
|
||||
const driverName = (
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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'];
|
||||
|
@ -8,10 +8,6 @@ export const ManageAgents = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="text-xl text-gray-600 py-3 font-bold">
|
||||
Data Connector Agents
|
||||
</p>
|
||||
<hr className="m-0" />
|
||||
<ManageAgentsTable />
|
||||
<Button onClick={() => setShowCreateAgentForm(true)}>Add Agent</Button>
|
||||
{showCreateAgentForm ? (
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user