console: Add a confirm dialog when removing a data source

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8728
GitOrigin-RevId: 1f90c60803dcddc78ef1af0618d2153214d7917a
This commit is contained in:
Matthew Goodwin 2023-04-12 15:28:50 -05:00 committed by hasura-bot
parent cd024dcaa4
commit 27a02cf2bc
5 changed files with 257 additions and 184 deletions

View File

@ -1,69 +1,32 @@
import { CardedTable } from '../../../../new-components/CardedTable';
import { Button } from '../../../../new-components/Button';
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 { useReloadSource } from '../../hooks/useReloadSource';
import { useDropSource } from '../../hooks/useDropSource';
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 to from 'await-to-js';
import React, { useState } from 'react';
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 { FaEdit, FaTrash, FaUndo } from 'react-icons/fa';
import Skeleton from 'react-loading-skeleton';
import globals from '../../../../Globals';
import { useUpdateProjectRegion } from '../../hooks/useUpdateProjectRegion';
import { useAppDispatch } from '../../../../storeHooks';
import _push from '../../../../components/Services/Data/push';
import { exportMetadata } from '../../../../metadata/actions';
import { MetadataDataSource } from '../../../../metadata/types';
import { useDestructiveAlert } from '../../../../new-components/Alert';
import { Button } from '../../../../new-components/Button';
import { CardedTable } from '../../../../new-components/CardedTable';
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
import { hasuraToast } from '../../../../new-components/Toasts';
import { useAppDispatch } from '../../../../storeHooks';
import { getProjectId, isCloudConsole } from '../../../../utils/cloudConsole';
import { useMetadata } from '../../../MetadataAPI';
import { useDatabaseLatencyCheck } from '../../hooks/useDatabaseLatencyCheck';
import { useDatabaseVersion } from '../../hooks/useDatabaseVersion';
import { useDropSource } from '../../hooks/useDropSource';
import { useInconsistentSources } from '../../hooks/useInconsistentSources';
import { useReloadSource } from '../../hooks/useReloadSource';
import { useUpdateProjectRegion } from '../../hooks/useUpdateProjectRegion';
import { Latency } from '../../types';
import { AccelerateProject, Details, LatencyBadge } from './parts';
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>
);
type DatabaseItem = {
dataSourceName: MetadataDataSource['name'];
driver: MetadataDataSource['kind'];
};
export const ListConnectedDatabases = (props?: { className?: string }) => {
@ -77,7 +40,6 @@ export const ListConnectedDatabases = (props?: { className?: string }) => {
} = 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);
@ -91,19 +53,18 @@ export const ListConnectedDatabases = (props?: { className?: string }) => {
},
});
const {
mutate: updateProjectRegionForRowId,
// isLoading: isUpdatingProjectRegion,
} = useUpdateProjectRegion();
const dispatch = useAppDispatch();
const [activeRow, setActiveRow] = useState<number>();
const { reloadSource, isLoading: isSourceReloading } = useReloadSource();
const { dropSource, isLoading: isSourceRemovalInProgress } = useDropSource({
onSuccess: () => {
dispatch(exportMetadata());
},
});
const {
data: inconsistentSources,
isLoading: isInconsistentFetchCallLoading,
@ -128,81 +89,124 @@ export const ListConnectedDatabases = (props?: { className?: string }) => {
const isCurrentRow = (rowIndex: number) => rowIndex === activeRow;
if (isLoading) return <>Loading...</>;
const columns = ['database', 'driver', '', ''];
const rowData = (databaseList ?? []).map((databaseItem, index) => [
<div>{databaseItem.dataSourceName}</div>,
databaseItem.driver,
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 && isCurrentRow(index)}
loadingText="Reloading"
>
Reload
</Button>
<Button
icon={<FaEdit />}
size="sm"
onClick={() => {
dispatch(
_push(
`/data/v2/manage/database/edit?driver=${databaseItem.driver}&database=${databaseItem.dataSourceName}`
)
);
}}
>
Edit
</Button>
<Button
icon={<FaTrash />}
mode="destructive"
size="sm"
onClick={() => {
dropSource({
driver: databaseItem.driver,
dataSourceName: databaseItem.dataSourceName,
});
}}
isLoading={isSourceRemovalInProgress && isCurrentRow(index)}
loadingText="Deleting"
>
Remove
</Button>
</div>,
]);
const handleEdit = React.useCallback((databaseItem: DatabaseItem) => {
dispatch(
_push(
`/data/v2/manage/database/edit?driver=${databaseItem.driver}&database=${databaseItem.dataSourceName}`
)
);
}, []);
const openUpdateProjectRegionPage = (_rowId?: string) => {
const { destructivePrompt } = useDestructiveAlert();
const handleRemove = React.useCallback(
(databaseItem: DatabaseItem) => {
destructivePrompt({
resourceName: databaseItem.dataSourceName,
resourceType: 'Data Source',
destroyTerm: 'remove',
onConfirm: async () => {
let success = true;
const [err] = await to(
dropSource({
driver: databaseItem.driver,
dataSourceName: databaseItem.dataSourceName,
})
);
if (err) {
success = false;
}
return success;
},
});
},
[dropSource]
);
const rowData = React.useMemo(
() =>
(databaseList ?? []).map((databaseItem, index) => [
<div>{databaseItem.dataSourceName}</div>,
databaseItem.driver,
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 => {
setActiveRow(index);
}}
>
<Button
icon={<FaUndo />}
size="sm"
onClick={() => reloadSource(databaseItem.dataSourceName)}
isLoading={isSourceReloading && isCurrentRow(index)}
loadingText="Reloading"
>
Reload
</Button>
<Button
icon={<FaEdit />}
size="sm"
onClick={() => handleEdit(databaseItem)}
>
Edit
</Button>
<Button
icon={<FaTrash />}
mode="destructive"
size="sm"
onClick={() => handleRemove(databaseItem)}
isLoading={isSourceRemovalInProgress && isCurrentRow(index)}
loadingText="Deleting"
>
Remove
</Button>
</div>,
]),
[
databaseList,
databaseVersions,
inconsistentSources,
isCurrentRow,
isDatabaseVersionLoading,
isInconsistentFetchCallLoading,
isSourceReloading,
isSourceRemovalInProgress,
latencies,
reloadSource,
]
);
const {
mutate: updateProjectRegionForRowId,
// isLoading: isUpdatingProjectRegion,
} = useUpdateProjectRegion();
const openUpdateProjectRegionPage = React.useCallback((_rowId?: string) => {
if (!_rowId) {
hasuraToast({
type: 'error',
@ -224,7 +228,9 @@ export const ListConnectedDatabases = (props?: { className?: string }) => {
const cloudDetailsPage = `${window.location.protocol}//${window.location.host}/project/${projectId}/details?open_update_region_drawer=true`;
window.open(cloudDetailsPage, '_blank');
};
}, []);
if (isLoading) return <>Loading...</>;
return (
<div className={props?.className}>
@ -242,47 +248,15 @@ export const ListConnectedDatabases = (props?: { className?: string }) => {
)}
{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>
<AccelerateProject
isLoading={databaseCheckLoading}
onReCheckClick={() => {
refetch();
}}
onUpdateRegionClick={() => {
openUpdateProjectRegionPage(rowId);
}}
/>
) : (
isCloudConsole(globals) && (
<Button

View File

@ -0,0 +1,57 @@
import { Button } from '../../../../../new-components/Button';
import { FaRedoAlt, FaExternalLinkAlt } from 'react-icons/fa';
import { IndicatorCard } from '../../../../../new-components/IndicatorCard';
import { LearnMoreLink } from '../../../../../new-components/LearnMoreLink';
import React from 'react';
export function AccelerateProject({
isLoading,
onReCheckClick,
onUpdateRegionClick,
}: {
isLoading: boolean;
onReCheckClick: () => void;
onUpdateRegionClick: () => void;
}) {
return (
<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={onReCheckClick}
isLoading={isLoading}
loadingText="Measuring Latencies..."
icon={<FaRedoAlt />}
>
Re-check Database Latency
</Button>
<Button
className="mr-xs"
onClick={onUpdateRegionClick}
icon={<FaExternalLinkAlt />}
>
Update Project Region
</Button>
</div>
</div>
</IndicatorCard>
</div>
);
}

View File

@ -23,9 +23,9 @@ export const DisplayDetails = ({
<div className="flex justify-start">
<div
className={clsx(
'max-w-2xl',
'max-w-md',
isExpanded
? 'whitespace-pre-line'
? 'whitespace-pre-line max-w-xl'
: 'overflow-hidden text-ellipsis whitespace-nowrap'
)}
title={version}

View File

@ -0,0 +1,38 @@
import { FaCheck, FaExclamationTriangle, FaMinusCircle } from 'react-icons/fa';
import { Latency } from '../../../types';
import { Badge } from '../../../../../new-components/Badge';
import React from 'react';
export 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>
);
};

View File

@ -0,0 +1,4 @@
export { AccelerateProject } from './AccelerateProject';
export { DisplayDetails, Details } from './Details';
export { InconsistentSourceDetails } from './InconsistentSourceDetails';
export { LatencyBadge } from './LatencyBadge';