console: Add edit/view for native queries

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9307
GitOrigin-RevId: dddc9c2701a2f7958c423a5ddda6eb35163f425d
This commit is contained in:
Matthew Goodwin 2023-05-31 16:02:26 -05:00 committed by hasura-bot
parent 682af1e2b9
commit 608e1bf29e
12 changed files with 314 additions and 188 deletions

View File

@ -37,9 +37,13 @@ import { TableEditItemContainer } from './TableEditItem/TableEditItemContainer';
import { TableInsertItemContainer } from './TableInsertItem/TableInsertItemContainer';
import { ModifyTableContainer } from './TableModify/ModifyTableContainer';
import { LandingPageRoute as NativeQueries } from '../../../features/Data/LogicalModels/LandingPage/LandingPage';
import { AddNativeQueryRoute } from '../../../features/Data/LogicalModels/AddNativeQuery/AddNativeQueryRoute';
import { TrackStoredProcedureRoute } from '../../../features/Data/LogicalModels/StoredProcedures/StoredProcedureWidget.route';
import { ManageFunction } from '../../../features/Data/ManageFunction/ManageFunction';
import {
UpdateNativeQueryRoute,
AddNativeQueryRoute,
} from '../../../features/Data/LogicalModels/AddNativeQuery';
const makeDataRouter = (
connect,
@ -82,6 +86,10 @@ const makeDataRouter = (
<Route path="native-queries">
<IndexRoute component={NativeQueries} />
<Route path="create" component={AddNativeQueryRoute} />
<Route
path="native-query/:source/:name"
component={UpdateNativeQueryRoute}
/>
<Route path="logical-models" component={NativeQueries} />
<Route path="stored-procedures" component={NativeQueries} />
<Route

View File

@ -7,8 +7,8 @@ export const AddNativeQueryRoute = withRouter<{
router: InjectedRouter;
}>(({ location, router }) => {
return (
<RouteWrapper pathname={location.pathname} push={router.push}>
<AddNativeQuery push={router.push} />
<RouteWrapper route={'/data/native-queries/create'}>
<AddNativeQuery />
</RouteWrapper>
);
});

View File

@ -92,7 +92,7 @@ export const Basic: Story = {
export const WithRouteWrapper: Story = {
render: args => (
<RouteWrapper pathname={'/data/native-queries/create'}>
<RouteWrapper route={'/data/native-queries/create'}>
<AddNativeQuery {...args} />
</RouteWrapper>
),

View File

@ -8,9 +8,11 @@ import {
useConsoleForm,
} from '../../../../new-components/Form';
// import { FormDebugWindow } from '../../../../new-components/Form/dev-components/FormDebugWindow';
import Skeleton from 'react-loading-skeleton';
import { Driver, drivers } from '../../../../dataSources';
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
import { hasuraToast } from '../../../../new-components/Toasts';
import { usePushRoute } from '../../../ConnectDBRedesign/hooks';
import { Feature } from '../../../DataSource';
import { useMetadata } from '../../../hasura-metadata-api';
import { useSupportedDataTypes } from '../../hooks/useSupportedDataTypes';
@ -21,16 +23,15 @@ import { SqlEditorField } from './components/SqlEditorField';
import { schema } from './schema';
import { NativeQueryForm } from './types';
import { transformFormOutputToMetadata } from './utils';
import Skeleton from 'react-loading-skeleton';
type AddNativeQueryProps = {
defaultFormValues?: Partial<NativeQueryForm>;
push?: (path: string) => void;
mode?: 'create' | 'update';
};
export const AddNativeQuery = ({
defaultFormValues,
push,
mode = 'create',
}: AddNativeQueryProps) => {
const {
Form,
@ -40,6 +41,8 @@ export const AddNativeQuery = ({
options: { defaultValues: defaultFormValues },
});
const push = usePushRoute();
const {
data: sources,
isLoading: isSourcesLoading,
@ -59,7 +62,7 @@ export const AddNativeQuery = ({
s => s.name === selectedSource
)?.logical_models;
const { trackNativeQuery, isLoading } = useTrackNativeQuery();
const { trackNativeQuery, isLoading: isSaving } = useTrackNativeQuery();
const [isLogicalModelsDialogOpen, setIsLogicalModelsDialogOpen] =
React.useState(false);
@ -77,7 +80,7 @@ export const AddNativeQuery = ({
toastOptions: { duration: 3000 },
});
// Go to list
push?.('/data/native-queries');
push('/data/native-queries');
},
onError: err => {
hasuraToast({
@ -127,96 +130,107 @@ export const AddNativeQuery = ({
);
return (
<Form onSubmit={handleFormSubmit}>
{/* <FormDebugWindow /> */}
<div className="max-w-xl flex flex-col">
<GraphQLSanitizedInputField
name="root_field_name"
label="Native Query Name"
placeholder="Name that exposes this model in GraphQL API"
hideTips
/>
<InputField
name="comment"
label="Comment"
placeholder="A description of this logical model"
/>
<Select
name="source"
label="Database"
// saving prop for future update
//noOptionsMessage="No databases found."
loading={isSourcesLoading}
options={(sources ?? []).map(m => ({
label: m.name,
value: m.name,
}))}
placeholder="Select a database..."
/>
</div>
{isIntrospectionLoading ? (
<div>
<Skeleton />
<Skeleton />
</div>
) : (
<ArgumentsField types={typeOptions} />
<div>
{mode === 'update' && (
<IndicatorCard status="info">
The current release does not support editing Native Queries. This
feature will be available in a future release. You can still edit
directly by modifying the Metadata.
</IndicatorCard>
)}
<SqlEditorField />
<div className="flex w-full">
{/* Logical Model Dropdown */}
<Select
name="returns"
selectClassName="max-w-xl"
// saving prop for future update
// noOptionsMessage={
// !selectedSource ? 'Select a database first.' : 'No models found.'
// }
// force component re-init on source change
//key={selectedSource}
label="Query Return Type"
placeholder={logicalModelSelectPlaceholder()}
loading={isSourcesLoading}
options={(logicalModels ?? []).map(m => ({
label: m.name,
value: m.name,
}))}
/>
<Button
icon={<FaPlusCircle />}
onClick={() => {
setIsLogicalModelsDialogOpen(true);
}}
>
Add Logical Model
</Button>
</div>
{isLogicalModelsDialogOpen ? (
<LogicalModelWidget
onCancel={() => {
setIsLogicalModelsDialogOpen(false);
}}
onSubmit={() => {
setIsLogicalModelsDialogOpen(false);
}}
asDialog
/>
) : null}
<div className="flex flex-row justify-end gap-2">
{/*
<Form onSubmit={handleFormSubmit}>
<fieldset disabled={mode === 'update'}>
{/* <FormDebugWindow /> */}
<div className="max-w-xl flex flex-col">
<GraphQLSanitizedInputField
name="root_field_name"
label="Native Query Name"
placeholder="Name that exposes this model in GraphQL API"
hideTips
/>
<InputField
name="comment"
label="Comment"
placeholder="A description of this logical model"
/>
<Select
name="source"
label="Database"
// saving prop for future update
//noOptionsMessage="No databases found."
loading={isSourcesLoading}
options={(sources ?? []).map(m => ({
label: m.name,
value: m.name,
}))}
placeholder="Select a database..."
/>
</div>
{isIntrospectionLoading ? (
<div>
<Skeleton />
<Skeleton />
</div>
) : (
<ArgumentsField types={typeOptions} />
)}
<SqlEditorField />
<div className="flex w-full">
{/* Logical Model Dropdown */}
<Select
name="returns"
selectClassName="max-w-xl"
// saving prop for future update
// noOptionsMessage={
// !selectedSource ? 'Select a database first.' : 'No models found.'
// }
// force component re-init on source change
//key={selectedSource}
label="Query Return Type"
placeholder={logicalModelSelectPlaceholder()}
loading={isSourcesLoading}
options={(logicalModels ?? []).map(m => ({
label: m.name,
value: m.name,
}))}
/>
<Button
icon={<FaPlusCircle />}
onClick={e => {
setIsLogicalModelsDialogOpen(true);
}}
>
Add Logical Model
</Button>
</div>
{isLogicalModelsDialogOpen ? (
<LogicalModelWidget
onCancel={() => {
setIsLogicalModelsDialogOpen(false);
}}
onSubmit={() => {
setIsLogicalModelsDialogOpen(false);
}}
asDialog
/>
) : null}
<div className="flex flex-row justify-end gap-2">
{/*
Validate Button will remain hidden until we have more information about how to handle standalone validation
Slack thread: https://hasurahq.slack.com/archives/C04LV93JNSH/p1682965503376129
*/}
{/* <Button icon={<FaPlay />}>Validate</Button> */}
<Button
type="submit"
icon={<FaSave />}
mode="primary"
isLoading={isLoading}
>
Save
</Button>
</div>
</Form>
{/* <Button icon={<FaPlay />}>Validate</Button> */}
<Button
type="submit"
icon={<FaSave />}
mode="primary"
isLoading={isSaving}
>
Save
</Button>
</div>
</fieldset>
</Form>
</div>
);
};

View File

@ -0,0 +1,58 @@
import { InjectedRouter, withRouter } from 'react-router';
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
import { MetadataUtils, useMetadata } from '../../../hasura-metadata-api';
import { RouteWrapper } from '../components/RouteWrapper';
import { AddNativeQuery } from './AddNativeQuery';
import { normalizeArguments } from './utils';
import Skeleton from 'react-loading-skeleton';
export const UpdateNativeQueryRoute = withRouter<{
location: Location;
router: InjectedRouter;
params: { source: string; name: string };
}>(({ location, router, params: { source, name } }) => {
if (!source || !name) {
return (
<IndicatorCard status="negative">
Unable to parse data from url.
</IndicatorCard>
);
}
const { data: sourceQueries, isLoading } = useMetadata(
m => MetadataUtils.findMetadataSource(source, m)?.native_queries
);
const nativeQuery = sourceQueries?.find(s => s.root_field_name === name);
if (!nativeQuery) {
return (
<IndicatorCard status="negative">
Native Query {name} not found in {source}
</IndicatorCard>
);
}
return (
<RouteWrapper
route={'/data/native-queries/{{source}}/{{name}}'}
itemSourceName={source}
itemName={name}
>
{isLoading ? (
<div>
<Skeleton count={10} />
</div>
) : (
<AddNativeQuery
mode={'update'}
defaultFormValues={{
...nativeQuery,
source: source,
arguments: normalizeArguments(nativeQuery.arguments ?? {}),
}}
/>
)}
</RouteWrapper>
);
});

View File

@ -0,0 +1,3 @@
export { AddNativeQueryRoute } from './AddNativeQuery.route';
export { AddNativeQuery } from './AddNativeQuery';
export { UpdateNativeQueryRoute } from './UpdateNativeQuery.route';

View File

@ -1,5 +1,8 @@
import { NativeQueryForm } from './types';
import { NativeQuery as NativeQueryMetadataType } from '../../../hasura-metadata-types';
import {
NativeQueryArgument,
NativeQuery as NativeQueryMetadataType,
} from '../../../hasura-metadata-types';
import { NativeQueryArgumentNormalized, NativeQueryForm } from './types';
export const transformFormOutputToMetadata = (
formValues: NativeQueryForm
@ -28,3 +31,11 @@ export const transformFormOutputToMetadata = (
type,
};
};
export const normalizeArguments = (
args: Record<string, NativeQueryArgument>
): NativeQueryArgumentNormalized[] =>
Object.entries(args).map(([name, argument]) => ({
name,
...argument,
}));

View File

@ -1,30 +1,25 @@
import { Tabs } from '../../../../new-components/Tabs';
import { useMetadata } from '../../../hasura-metadata-api';
import { ListLogicalModels } from './components/ListLogicalModels';
import { ListNativeQueries } from './components/ListNativeQueries';
import { extractModelsAndQueriesFromMetadata } from '../utils';
import {
useDestructiveAlert,
useHasuraAlert,
} from '../../../../new-components/Alert';
import { LogicalModelWithSource, NativeQueryWithSource } from '../types';
import { useTrackNativeQuery } from '../../hooks/useTrackNativeQuery';
import { useTrackLogicalModel } from '../../hooks/useTrackLogicalModel';
import { hasuraToast } from '../../../../new-components/Toasts';
import { useState } from 'react';
import { InjectedRouter, Link, withRouter } from 'react-router';
import { LogicalModelWidget } from '../LogicalModelWidget/LogicalModelWidget';
import { useDestructiveAlert } from '../../../../new-components/Alert';
import { Button } from '../../../../new-components/Button';
import { Tabs } from '../../../../new-components/Tabs';
import { hasuraToast } from '../../../../new-components/Toasts';
import { usePushRoute } from '../../../ConnectDBRedesign/hooks';
import { useMetadata } from '../../../hasura-metadata-api';
import { useTrackLogicalModel } from '../../hooks/useTrackLogicalModel';
import { useTrackNativeQuery } from '../../hooks/useTrackNativeQuery';
import { LogicalModelWidget } from '../LogicalModelWidget/LogicalModelWidget';
import { RouteWrapper } from '../components/RouteWrapper';
import { ListStoredProcedures } from './components/ListStoredProcedures';
export const LandingPage = ({
push,
pathname,
}: {
pathname: string | undefined;
push?: (to: string) => void;
}) => {
import { LogicalModelWithSource, NativeQueryWithSource } from '../types';
import { extractModelsAndQueriesFromMetadata } from '../utils';
import { ListLogicalModels } from './components/ListLogicalModels';
import { ListNativeQueries } from './components/ListNativeQueries';
import { ListStoredProcedures } from './components/ListStoredProcedures';
import { NATIVE_QUERY_ROUTES } from '../constants';
export const LandingPage = ({ pathname }: { pathname: string }) => {
const push = usePushRoute();
const { data, isLoading } = useMetadata(m =>
extractModelsAndQueriesFromMetadata(m)
);
@ -44,8 +39,6 @@ export const LandingPage = ({
const [isLogicalModelsDialogOpen, setIsLogicalModelsDialogOpen] =
useState(false);
const { hasuraAlert } = useHasuraAlert();
const { destructiveConfirm } = useDestructiveAlert();
const { untrackNativeQuery } = useTrackNativeQuery();
@ -133,12 +126,10 @@ export const LandingPage = ({
<ListNativeQueries
nativeQueries={nativeQueries}
isLoading={isLoading}
onEditClick={() => {
hasuraAlert({
title: 'Not Implemented',
message:
'Editing is not implemented in the alpha release',
});
onEditClick={query => {
push?.(
`data/native-queries/native-query/${query.source.name}/${query.root_field_name}`
);
}}
onRemoveClick={handleRemoveNativeQuery}
/>
@ -202,8 +193,8 @@ export const LandingPageRoute = withRouter<{
router: InjectedRouter;
}>(({ location, router }) => {
return (
<RouteWrapper pathname={location.pathname} push={router.push}>
<LandingPage pathname={location.pathname} push={router.push} />
<RouteWrapper route={location.pathname as keyof typeof NATIVE_QUERY_ROUTES}>
<LandingPage pathname={location.pathname} />
</RouteWrapper>
);
});

View File

@ -4,10 +4,12 @@ import {
useReactTable,
} from '@tanstack/react-table';
import React from 'react';
import { CgDetailsMore } from 'react-icons/cg';
import { FaTrash } from 'react-icons/fa';
import Skeleton from 'react-loading-skeleton';
import { Button } from '../../../../../new-components/Button';
import { NativeQueryWithSource } from '../../types';
import { CardedTableFromReactTable } from '../../components/CardedTableFromReactTable';
import { NativeQueryWithSource } from '../../types';
const columnHelper = createColumnHelper<NativeQueryWithSource>();
@ -44,10 +46,18 @@ export const ListNativeQueries = ({
header: 'Actions',
cell: ({ cell, row }) => (
<div className="flex flex-row gap-2">
{/* Re add once we implement Edit functionality */}
{/* <Button onClick={() => onEditClick(row.original)}>Edit</Button> */}
<Button
// icon={<FaEdit />}
icon={<CgDetailsMore />}
onClick={() => onEditClick(row.original)}
>
{/* Edit */}
{/* Change back to Edit once we support it */}
View
</Button>
<Button
mode="destructive"
icon={<FaTrash />}
onClick={() => onRemoveClick(row.original)}
>
Remove

View File

@ -1,9 +1,11 @@
import Skeleton from 'react-loading-skeleton';
import { LimitedFeatureWrapper } from '../../../ConnectDBRedesign/components/LimitedFeatureWrapper/LimitedFeatureWrapper';
import { useServerConfig } from '../../../../hooks';
import { ReactNode } from 'react';
import { Breadcrumbs } from '../../../../new-components/Breadcrumbs';
import startCase from 'lodash/startCase';
import { ReactNode } from 'react';
import Skeleton from 'react-loading-skeleton';
import { useServerConfig } from '../../../../hooks';
import { Breadcrumbs } from '../../../../new-components/Breadcrumbs';
import { LimitedFeatureWrapper } from '../../../ConnectDBRedesign/components/LimitedFeatureWrapper/LimitedFeatureWrapper';
import { usePushRoute } from '../../../ConnectDBRedesign/hooks';
import { NATIVE_QUERY_ROUTES } from '../constants';
function NativeQueriesFeatureFlag({ children }: { children: ReactNode }) {
const { data: serverConfig, isLoading: isLoadingServerConfig } =
@ -32,53 +34,21 @@ function NativeQueriesFeatureFlag({ children }: { children: ReactNode }) {
return <>{children}</>;
}
type AllowedTabs =
| 'logical-models'
| 'native-queries'
| 'stored-procedures'
| 'create';
const getTitleAndSubtitle = (tabType: AllowedTabs) => {
if (tabType === 'logical-models')
return {
title: 'Logical Models',
subtitle:
'Creating Logical Models in advance can help generate Native Queries faster',
};
if (tabType === 'native-queries')
return {
title: 'Native Queries',
subtitle:
'Access more queries and operators through SQL on your database',
};
if (tabType === 'stored-procedures')
return {
title: 'Stored Procedures',
subtitle: 'Add support for stored procedures on SQL over a GraphQL API',
};
if (tabType === 'create')
return {
title: 'Create Native Query',
subtitle:
'Access more queries and operators through SQL on your database',
};
return {
title: 'Not a valid path',
subtitle: '',
};
};
export const RouteWrapper: React.FC<{
route: keyof typeof NATIVE_QUERY_ROUTES;
itemSourceName?: string;
itemName?: string;
}> = ({ children, route, itemSourceName, itemName }) => {
const paths =
route
?.split('/')
.filter(Boolean)
.filter(p => p !== '{{source}}') ?? [];
const { title, subtitle } = NATIVE_QUERY_ROUTES[route];
const push = usePushRoute();
export const RouteWrapper = ({
children,
pathname,
push,
}: {
children: ReactNode;
pathname: string | undefined;
push?: (to: string) => void;
}) => {
const paths = pathname?.split('/').filter(Boolean) ?? [];
const tabType = paths[paths.length - 1] as AllowedTabs;
const { title, subtitle } = getTitleAndSubtitle(tabType);
return (
<div className="py-md px-md w-full">
<LimitedFeatureWrapper
@ -91,19 +61,26 @@ export const RouteWrapper = ({
<Breadcrumbs
items={paths.map((path: string, index) => {
return {
title: startCase(path),
title: startCase(
path
// we don't need to display source
.replace('{{source}}', itemSourceName ?? '')
.replace('{{name}}', itemName ?? '')
),
onClick:
index === paths.length - 1
? undefined
: () => {
push?.(`/${paths.slice(0, index + 1).join('/')}`);
push(`/${paths.slice(0, index + 1).join('/')}`);
},
};
})}
/>
<div className="flex w-full justify-between px-2">
<div className="mb-sm">
<div className="text-xl font-bold mt-2">{title}</div>
<div className="text-xl font-bold mt-2">
{title.replace('{{name}}', itemName ?? '')}
</div>
<div className="text-muted">{subtitle}</div>
</div>
</div>

View File

@ -8,3 +8,45 @@ export const STORED_PROCEDURE_UNTRACK_SUCCESS =
'Successfully untracked Stored Procedure';
export const STORED_PROCEDURE_UNTRACK_ERROR =
'Unable to untrack Stored Procedure';
export const NATIVE_QUERY_ROUTES = {
'/data/native-queries': {
title: 'Native Queries',
subtitle: 'Access more queries and operators through SQL on your database',
},
'/data/native-queries/create': {
title: 'Create Native Query',
subtitle: 'Access more queries and operators through SQL on your database',
},
'/data/native-queries/{{source}}/{{name}}': {
title: '{{name}}',
subtitle: 'Access more queries and operators through SQL on your database',
},
'/data/native-queries/logical-models': {
title: 'Logical Models',
subtitle:
'Creating Logical Models in advance can help generate Native Queries faster',
},
'/data/native-queries/logical-models/create': {
title: 'Logical Models',
subtitle:
'Creating Logical Models in advance can help generate Native Queries faster',
},
'/data/native-queries/logical-models/{{source}}/{{name}}': {
title: '{{name}}',
subtitle:
'Creating Logical Models in advance can help generate Native Queries faster',
},
'/data/native-queries/stored-procedures': {
title: 'Stored Procedures',
subtitle: 'Add support for stored procedures on SQL over a GraphQL API',
},
'/data/native-queries/stored-procedures/track': {
title: 'Track Stored Procedure',
subtitle: 'Expose your stored SQL procedures via the GraphQL API',
},
'/data/native-queries/stored-procedures/{{source}}/{{name}}': {
title: 'Track Stored Procedure',
subtitle: 'Expose your stored SQL procedures via the GraphQL API',
},
};

View File

@ -80,6 +80,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
loadingText,
disabled,
full,
...otherHtmlAttributes
} = props;
@ -87,9 +88,20 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
const styles = twButtonStyles;
const buttonAttributes = {
const buttonAttributes: typeof props = {
type,
...otherHtmlAttributes,
onClick: e => {
if (
e.target instanceof HTMLElement &&
e.target.closest('fieldset:disabled')
) {
//this prevents clicks when a fieldset enclosing this button is set to disabled.
// this is due to a bug in react that's been documented here: https://github.com/facebook/react/issues/7711
return;
}
props?.onClick?.(e);
},
disabled: isDisabled,
className: clsx(
styles.all,