console: Refactor duplicated logical model and add native query wrappers into one

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9199
GitOrigin-RevId: 87ded79e4130a9622b6733055882cee9096088ba
This commit is contained in:
Julian 2023-05-18 17:52:11 -03:00 committed by hasura-bot
parent 62304a9182
commit 6caf33bf27
6 changed files with 304 additions and 309 deletions

View File

@ -18,7 +18,7 @@ export default {
} as ComponentMeta<typeof AddNativeQuery>;
export const Basic: ComponentStory<typeof AddNativeQuery> = args => {
return <AddNativeQuery pathname="/data/native-queries/create" />;
return <AddNativeQuery />;
};
const fillAndSubmitForm = async ({
@ -83,7 +83,6 @@ const fillAndSubmitForm = async ({
export const HappyPath: ComponentStory<typeof AddNativeQuery> = args => {
return (
<AddNativeQuery
pathname="/data/native-queries/create"
defaultFormValues={{
code: `SELECT * FROM (VALUES ('hello', 'world'), ('welcome', 'friend')) as t("one", "two")`,
}}
@ -112,7 +111,6 @@ HappyPath.play = async ({ canvasElement }) => {
export const ErrorExists: ComponentStory<typeof AddNativeQuery> = args => {
return (
<AddNativeQuery
pathname="/data/native-queries/create"
defaultFormValues={{
code: `SELECT * FROM (VALUES ('hello', 'world'), ('welcome', 'friend')) as t("one", "two")`,
}}
@ -146,7 +144,6 @@ ErrorExists.play = async ({ canvasElement }) => {
export const ErrorValidation: ComponentStory<typeof AddNativeQuery> = args => {
return (
<AddNativeQuery
pathname="/data/native-queries/create"
defaultFormValues={{
code: `select * from foo`,
}}
@ -180,7 +177,6 @@ ErrorValidation.play = async ({ canvasElement }) => {
export const ErrorDisabled: ComponentStory<typeof AddNativeQuery> = args => {
return (
<AddNativeQuery
pathname="/data/native-queries/create"
defaultFormValues={{
code: `SELECT * FROM (VALUES ('hello', 'world'), ('welcome', 'friend')) as t("one", "two")`,
}}

View File

@ -8,7 +8,6 @@ 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';
@ -18,20 +17,19 @@ import { useSupportedDataTypes } from '../../hooks/useSupportedDataTypes';
import { useTrackNativeQuery } from '../../hooks/useTrackNativeQuery';
import { LogicalModelWidget } from '../LogicalModelWidget/LogicalModelWidget';
import { ArgumentsField } from './components/ArgumentsField';
import { PageWrapper } from './components/PageWrapper';
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>;
pathname: string;
push?: (path: string) => void;
};
export const AddNativeQuery = ({
defaultFormValues,
pathname,
push,
}: AddNativeQueryProps) => {
const {
@ -129,93 +127,91 @@ export const AddNativeQuery = ({
);
return (
<PageWrapper pathname={pathname} push={push}>
<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..."
/>
<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>
{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={() => {
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">
{/*
) : (
<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={() => {
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">
Save
</Button>
</div>
</Form>
</PageWrapper>
{/* <Button icon={<FaPlay />}>Validate</Button> */}
<Button type="submit" icon={<FaSave />} mode="primary">
Save
</Button>
</div>
</Form>
);
};

View File

@ -1,9 +1,14 @@
import { InjectedRouter, withRouter } from 'react-router';
import { AddNativeQuery } from './AddNativeQuery';
import { RouteWrapper } from '../components/RouteWrapper';
export const AddNativeQueryRoute = withRouter<{
location: Location;
router: InjectedRouter;
}>(({ location, router }) => {
return <AddNativeQuery pathname={location.pathname} push={router.push} />;
return (
<RouteWrapper pathname={location.pathname} push={router.push}>
<AddNativeQuery push={router.push} />
</RouteWrapper>
);
});

View File

@ -1,35 +0,0 @@
import { Breadcrumbs } from '../../../../../new-components/Breadcrumbs';
import startCase from 'lodash/startCase';
export const PageWrapper: React.FC<{
pathname: string | undefined;
push?: (to: string) => void;
}> = ({ children, push, pathname }) => {
const paths = pathname?.split('/').filter(Boolean) ?? [];
return (
<div className="flex flex-col">
<div className="py-md px-md w-full">
<Breadcrumbs
items={paths.map((path: string, index) => {
return {
title: startCase(path),
onClick:
index === paths.length - 1
? undefined
: () => {
push?.(`/${paths.slice(0, index + 1).join('/')}`);
},
};
})}
/>
<div className="w-full">
<div className="text-xl font-bold mt-2">Create Native Query</div>
<div className="text-muted">
Access more queries and operators through SQL on your database.
</div>
</div>
</div>
<div className="px-md w-full flex flex-col">{children}</div>
</div>
);
};

View File

@ -1,4 +1,3 @@
import { Breadcrumbs } from '../../../../new-components/Breadcrumbs';
import { Tabs } from '../../../../new-components/Tabs';
import { useMetadata } from '../../../hasura-metadata-api';
import { ListLogicalModels } from './components/ListLogicalModels';
@ -12,44 +11,16 @@ import { LogicalModelWithSource, NativeQueryWithSource } from '../types';
import { useTrackNativeQuery } from '../../hooks/useTrackNativeQuery';
import { useTrackLogicalModel } from '../../hooks/useTrackLogicalModel';
import { hasuraToast } from '../../../../new-components/Toasts';
import { ReactNode, useState } from 'react';
import { useState } from 'react';
import { InjectedRouter, Link, withRouter } from 'react-router';
import { LogicalModelWidget } from '../LogicalModelWidget/LogicalModelWidget';
import { Button } from '../../../../new-components/Button';
import startCase from 'lodash/startCase';
import { LimitedFeatureWrapper } from '../../../ConnectDBRedesign/components/LimitedFeatureWrapper/LimitedFeatureWrapper';
import { useServerConfig } from '../../../../hooks';
import Skeleton from 'react-loading-skeleton';
import { RouteWrapper } from '../components/RouteWrapper';
import { ListStoredProcedures } from '../StoredProcedures/ListStoredProcedures';
type AllowedTabs = 'logical-models' | 'native-queries' | 'stored-procedures';
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',
};
return {
title: 'Not a valid path',
subtitle: '',
};
};
export const LandingPage = ({
pathname,
push,
pathname,
}: {
pathname: string | undefined;
push?: (to: string) => void;
@ -69,9 +40,6 @@ export const LandingPage = ({
);
const nativeQueries = data?.queries ?? [];
const logicalModels = data?.models ?? [];
const paths = pathname?.split('/').filter(Boolean) ?? [];
const tabType = paths[paths.length - 1] as AllowedTabs;
const { title, subtitle } = getTitleAndSubtitle(tabType);
const [isLogicalModelsDialogOpen, setIsLogicalModelsDialogOpen] =
useState(false);
@ -135,158 +103,107 @@ export const LandingPage = ({
}),
});
};
return (
<div className="py-md px-md w-full">
<LimitedFeatureWrapper
title="Looking to add Native Queries?"
id="native-queries"
description="Get production-ready today with a 30-day free trial of Hasura EE, no credit card required."
>
<NativeQueriesFeatureFlag>
<div className="flex flex-col">
<Breadcrumbs
items={paths.map((path: string, index) => {
return {
title: startCase(path),
onClick:
index === paths.length - 1
? undefined
: () => {
push?.(paths.slice(0, index + 1).join('/'));
},
};
})}
/>
<div className="flex w-full justify-between">
<div className="mb-sm">
<div className="text-xl font-bold mt-2">{title}</div>
<div className="text-muted">{subtitle}</div>
</div>
</div>
{isLogicalModelsDialogOpen ? (
<LogicalModelWidget
onCancel={() => {
setIsLogicalModelsDialogOpen(false);
}}
onSubmit={() => {
setIsLogicalModelsDialogOpen(false);
}}
asDialog
/>
) : null}
<Tabs
defaultValue={pathname}
onValueChange={value => {
push?.(value);
}}
items={[
{
value: '/data/native-queries',
label: `Native Queries (${nativeQueries.length})`,
content: (
<div className="mt-md">
<ListNativeQueries
nativeQueries={nativeQueries}
isLoading={isLoading}
onEditClick={() => {
hasuraAlert({
title: 'Not Implemented',
message:
'Editing is not implemented in the alpha release',
});
}}
onRemoveClick={handleRemoveNativeQuery}
/>
<div className="flex justify-end mt-sm">
<Link to="/data/native-queries/create">
<Button mode="primary">Create Native Query</Button>
</Link>
</div>
</div>
),
},
{
value: '/data/native-queries/logical-models',
label: `Logical Models (${logicalModels.length})`,
content: (
<div className="mt-md">
<ListLogicalModels
logicalModels={logicalModels}
isLoading={isLoading}
onEditClick={() => {
hasuraAlert({
title: 'Not Implemented',
message:
'Editing is not implemented in the alpha release',
});
}}
onRemoveClick={handleRemoveLogicalModel}
/>
<div className="flex justify-end mt-sm">
<Button
mode="primary"
onClick={() => {
setIsLogicalModelsDialogOpen(true);
}}
>
Add Logical Model
</Button>
</div>
</div>
),
},
{
value: '/data/native-queries/stored-procedures',
label: `Stored Procedures (${storedProcedures.length})`,
content: (
<div className="mt-md">
<ListStoredProcedures />
<div className="flex justify-end mt-sm">
<Link to="/data/native-queries/stored-procedures/track">
<Button mode="primary">Track Stored Procedure</Button>
</Link>
</div>
</div>
),
},
]}
/>
</div>
</NativeQueriesFeatureFlag>
</LimitedFeatureWrapper>
<div className="w-full">
<div className="flex flex-col">
{isLogicalModelsDialogOpen ? (
<LogicalModelWidget
onCancel={() => {
setIsLogicalModelsDialogOpen(false);
}}
onSubmit={() => {
setIsLogicalModelsDialogOpen(false);
}}
asDialog
/>
) : null}
<Tabs
key={pathname}
defaultValue={pathname}
onValueChange={value => {
push?.(value);
}}
items={[
{
value: '/data/native-queries',
label: `Native Queries (${nativeQueries.length})`,
content: (
<div className="mt-md">
<ListNativeQueries
nativeQueries={nativeQueries}
isLoading={isLoading}
onEditClick={() => {
hasuraAlert({
title: 'Not Implemented',
message:
'Editing is not implemented in the alpha release',
});
}}
onRemoveClick={handleRemoveNativeQuery}
/>
<div className="flex justify-end mt-sm">
<Link to="/data/native-queries/create">
<Button mode="primary">Create Native Query</Button>
</Link>
</div>
</div>
),
},
{
value: '/data/native-queries/logical-models',
label: `Logical Models (${logicalModels.length})`,
content: (
<div className="mt-md">
<ListLogicalModels
logicalModels={logicalModels}
isLoading={isLoading}
onEditClick={() => {
push?.('/data/native-queries/logical-models/permissions');
}}
onRemoveClick={handleRemoveLogicalModel}
/>
<div className="flex justify-end mt-sm">
<Button
mode="primary"
onClick={() => {
setIsLogicalModelsDialogOpen(true);
}}
>
Add Logical Model
</Button>
</div>
</div>
),
},
{
value: '/data/native-queries/stored-procedures',
label: `Stored Procedures (${storedProcedures.length})`,
content: (
<div className="mt-md">
<ListStoredProcedures />
<div className="flex justify-end mt-sm">
<Link to="/data/native-queries/stored-procedures/track">
<Button mode="primary">Track Stored Procedure</Button>
</Link>
</div>
</div>
),
},
]}
/>
</div>
</div>
);
};
function NativeQueriesFeatureFlag({ children }: { children: ReactNode }) {
const { data: serverConfig, isLoading: isLoadingServerConfig } =
useServerConfig();
if (isLoadingServerConfig) {
return <Skeleton count={2} />;
}
const areNativeQueriesEnabled = serverConfig?.feature_flags?.find(
feature => feature.name === 'native-query-interface' && feature.enabled
);
if (!areNativeQueriesEnabled) {
return (
<div className="bg-white border-2 shadow-sm p-5 rounded space-y-4">
<div className="text-xl text-slate-900">
Looking to try Native Queries?
</div>
<div className="text-base text-muted max-w-2xl">
Native Queries are not enabled. To enable them, start the Hasura
server with the environment variable
<code>HASURA_FF_NATIVE_QUERY_INTERFACE: 'True'</code>
</div>
</div>
);
}
return children as JSX.Element;
}
export const LandingPageRoute = withRouter<{
location: Location;
router: InjectedRouter;
}>(({ location, router }) => {
return <LandingPage pathname={location.pathname} push={router.push} />;
return (
<RouteWrapper pathname={location.pathname} push={router.push}>
<LandingPage pathname={location.pathname} push={router.push} />
</RouteWrapper>
);
});

View File

@ -0,0 +1,116 @@
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';
function NativeQueriesFeatureFlag({ children }: { children: ReactNode }) {
const { data: serverConfig, isLoading: isLoadingServerConfig } =
useServerConfig();
if (isLoadingServerConfig) {
return <Skeleton count={2} />;
}
const areNativeQueriesEnabled = serverConfig?.feature_flags?.find(
feature => feature.name === 'native-query-interface' && feature.enabled
);
if (!areNativeQueriesEnabled) {
return (
<div className="bg-white border-2 shadow-sm p-5 rounded space-y-4">
<div className="text-xl text-slate-900">
Looking to try Native Queries?
</div>
<div className="text-base text-muted max-w-2xl">
Native Queries are not enabled. To enable them, start the Hasura
server with the environment variable
<code>HASURA_FF_NATIVE_QUERY_INTERFACE: 'True'</code>
</div>
</div>
);
}
// eslint-disable-next-line react/jsx-no-useless-fragment
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 = ({
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
title="Looking to add Native Queries?"
id="native-queries"
description="Get production-ready today with a 30-day free trial of Hasura EE, no credit card required."
>
<NativeQueriesFeatureFlag>
<div className="flex flex-col">
<Breadcrumbs
items={paths.map((path: string, index) => {
return {
title: startCase(path),
onClick:
index === paths.length - 1
? undefined
: () => {
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-muted">{subtitle}</div>
</div>
</div>
<div className="">{children}</div>
</div>
</NativeQueriesFeatureFlag>
</LimitedFeatureWrapper>
</div>
);
};