mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +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,
|
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>
|
||||||
|
@ -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,
|
||||||
|
@ -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(),
|
||||||
|
};
|
||||||
|
@ -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'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>
|
</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')
|
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({}));
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
@ -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 = {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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',
|
||||||
|
@ -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',
|
||||||
|
@ -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',
|
||||||
|
@ -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,
|
||||||
|
@ -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 = (
|
||||||
|
@ -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',
|
||||||
|
@ -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',
|
||||||
|
@ -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'];
|
||||||
|
@ -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 ? (
|
||||||
|
@ -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';
|
||||||
|
@ -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