mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
console: create a reusable database selector widget
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4295 GitOrigin-RevId: 962e0f1e25d0100eb452cd609401ca593963e188
This commit is contained in:
parent
abb57e58c8
commit
fcdc90f2d1
@ -0,0 +1,181 @@
|
|||||||
|
import { Button } from '@/new-components/Button';
|
||||||
|
import { Form } from '@/new-components/Form';
|
||||||
|
import { userEvent, within } from '@storybook/testing-library';
|
||||||
|
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
|
||||||
|
import { ComponentMeta, Story } from '@storybook/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { expect } from '@storybook/jest';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { DatabaseSelector } from '@/features/Data';
|
||||||
|
import { handlers } from './mocks/handlers.mock';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Data/components/DatabaseSelector',
|
||||||
|
component: DatabaseSelector,
|
||||||
|
decorators: [ReactQueryDecorator()],
|
||||||
|
parameters: {
|
||||||
|
msw: handlers(),
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof DatabaseSelector>;
|
||||||
|
|
||||||
|
export const Playground: Story = () => {
|
||||||
|
return (
|
||||||
|
<DatabaseSelector
|
||||||
|
value={{ database: 'chinook', schema: 'public', table: 'Album' }}
|
||||||
|
onChange={v => console.log(v)}
|
||||||
|
name="source"
|
||||||
|
className="border-l-4 border-l-green-600"
|
||||||
|
labels={{
|
||||||
|
database: 'My Database',
|
||||||
|
schema: 'My Schema',
|
||||||
|
dataset: 'My Dataset',
|
||||||
|
table: 'My Table',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithDisabledInputs: Story = () => {
|
||||||
|
return (
|
||||||
|
<DatabaseSelector
|
||||||
|
value={{ database: 'chinook', schema: 'public', table: 'Album' }}
|
||||||
|
onChange={v => console.log(v)}
|
||||||
|
name="source"
|
||||||
|
className="border-l-4 border-l-green-600"
|
||||||
|
disabledKeys={['database', 'schema', 'table']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BqWithDisabledInputs: Story = () => {
|
||||||
|
return (
|
||||||
|
<DatabaseSelector
|
||||||
|
value={{ database: 'bigquery_test', dataset: 'sensei', table: 'table1' }}
|
||||||
|
onChange={v => console.log(v)}
|
||||||
|
name="source"
|
||||||
|
className="border-l-4 border-l-green-600"
|
||||||
|
disabledKeys={['database', 'schema', 'table']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithHiddenInputs: Story = () => {
|
||||||
|
return (
|
||||||
|
<DatabaseSelector
|
||||||
|
value={{ database: 'bigquery_test', dataset: 'sensei', table: '' }}
|
||||||
|
onChange={v => console.log(v)}
|
||||||
|
name="source"
|
||||||
|
className="border-l-4 border-l-green-600"
|
||||||
|
hiddenKeys={['database']}
|
||||||
|
disabledKeys={['schema']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
destination: z.object({
|
||||||
|
database: z.string().min(1, 'Database is a required field!'),
|
||||||
|
schema: z.string().optional(),
|
||||||
|
dataset: z.string().optional(),
|
||||||
|
table: z.string().min(1, 'Table is a required field!'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const FormElements = () => {
|
||||||
|
const { control } = useFormContext<Schema>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="destination"
|
||||||
|
render={({ field: { onChange, value }, formState: { errors } }) => (
|
||||||
|
<DatabaseSelector
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
name="destination"
|
||||||
|
errors={errors}
|
||||||
|
className="border-l-4 border-l-green-600"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithReactFormHookNested: Story = () => {
|
||||||
|
const submit = (values: Schema) => {
|
||||||
|
console.log(JSON.stringify(values));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
options={{
|
||||||
|
defaultValues: {
|
||||||
|
destination: {
|
||||||
|
database: '',
|
||||||
|
schema: '',
|
||||||
|
table: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onSubmit={submit}
|
||||||
|
schema={schema}
|
||||||
|
>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormElements />
|
||||||
|
<Button type="submit" data-testid="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
WithReactFormHookNested.play = async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const sourceLabel = await canvas.findByText('Source');
|
||||||
|
const schemaLabel = await canvas.findByText('Schema');
|
||||||
|
const tableLabel = await canvas.findByText('Table');
|
||||||
|
|
||||||
|
// expect labels
|
||||||
|
expect(sourceLabel).toBeInTheDocument();
|
||||||
|
expect(schemaLabel).toBeInTheDocument();
|
||||||
|
expect(tableLabel).toBeInTheDocument();
|
||||||
|
|
||||||
|
const submitButton = await canvas.getByTestId('submit');
|
||||||
|
|
||||||
|
// update fields
|
||||||
|
const dbInput = await canvas.getByTestId('destination_database');
|
||||||
|
userEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await canvas.findByText('Database is a required field!')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
userEvent.selectOptions(dbInput, 'chinook');
|
||||||
|
|
||||||
|
const schemaInput = await canvas.getByTestId('destination_schema');
|
||||||
|
userEvent.selectOptions(schemaInput, 'public');
|
||||||
|
|
||||||
|
userEvent.click(submitButton);
|
||||||
|
expect(
|
||||||
|
await canvas.findByText('Table is a required field!')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
const tableInput = await canvas.getByTestId('destination_table');
|
||||||
|
userEvent.selectOptions(tableInput, 'Album');
|
||||||
|
|
||||||
|
// select a bigquery source
|
||||||
|
userEvent.selectOptions(dbInput, 'bigquery_test');
|
||||||
|
userEvent.selectOptions(schemaInput, 'sensei');
|
||||||
|
userEvent.selectOptions(tableInput, 'table1');
|
||||||
|
};
|
@ -0,0 +1,172 @@
|
|||||||
|
import { DataTarget } from '@/features/Datasources';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import get from 'lodash.get';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { FieldError } from 'react-hook-form';
|
||||||
|
import { FaDatabase, FaFolder, FaTable } from 'react-icons/fa';
|
||||||
|
import { useSourcesTree } from './hooks/useSourceTree';
|
||||||
|
import { Select } from './Select';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: DataTarget;
|
||||||
|
disabledKeys?: string[];
|
||||||
|
onChange?: (value: DataTarget) => void;
|
||||||
|
name: string;
|
||||||
|
errors?: Record<string, any>;
|
||||||
|
className?: string;
|
||||||
|
hiddenKeys?: string[];
|
||||||
|
labels?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DatabaseSelector = (props: Props) => {
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
name,
|
||||||
|
errors,
|
||||||
|
disabledKeys,
|
||||||
|
hiddenKeys,
|
||||||
|
labels,
|
||||||
|
} = props;
|
||||||
|
const maybeError = get(errors, name) as
|
||||||
|
| { database?: FieldError; schema?: FieldError; table?: FieldError }
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const [localValue, setLocalValue] = useState<{
|
||||||
|
database: string;
|
||||||
|
schema: string;
|
||||||
|
table: string;
|
||||||
|
}>({
|
||||||
|
database: value.database,
|
||||||
|
schema: (value as any).schema ?? (value as any).dataset,
|
||||||
|
table: value.table,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { tree, isLoading } = useSourcesTree();
|
||||||
|
|
||||||
|
const schemaOptions = tree[localValue.database]
|
||||||
|
? Object.keys(tree[localValue.database].children)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const tableOptions = tree[localValue.database]?.children?.[localValue.schema]
|
||||||
|
? tree[localValue.database]?.children?.[localValue.schema]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const updateParent = (val: {
|
||||||
|
database: string;
|
||||||
|
schema: string;
|
||||||
|
table: string;
|
||||||
|
}) => {
|
||||||
|
if (!onChange) return;
|
||||||
|
|
||||||
|
if (tree[val.database].kind !== 'bigquery')
|
||||||
|
onChange({
|
||||||
|
database: val.database,
|
||||||
|
schema: val.schema,
|
||||||
|
table: val.table,
|
||||||
|
});
|
||||||
|
else
|
||||||
|
onChange({
|
||||||
|
database: val.database,
|
||||||
|
dataset: val.schema,
|
||||||
|
table: val.table,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <>Loading data ...</>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="source"
|
||||||
|
className={clsx(
|
||||||
|
`grid grid-cols-12 rounded bg-gray-50 border border-gray-300 p-md gap-y-4 ${props.className}`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'col-span-12',
|
||||||
|
hiddenKeys?.includes('database') ? 'hidden' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={Object.keys(tree)}
|
||||||
|
value={localValue.database}
|
||||||
|
disabled={!Object.keys(tree) || disabledKeys?.includes('database')}
|
||||||
|
onChange={e => {
|
||||||
|
e.persist();
|
||||||
|
const temp = {
|
||||||
|
database: e.target.value,
|
||||||
|
schema: '',
|
||||||
|
table: '',
|
||||||
|
};
|
||||||
|
setLocalValue(temp);
|
||||||
|
updateParent(temp);
|
||||||
|
}}
|
||||||
|
placeholder="Select a database..."
|
||||||
|
error={maybeError?.database}
|
||||||
|
data-testid={`${name}_database`}
|
||||||
|
icon={<FaDatabase />}
|
||||||
|
label={labels?.database ?? 'Source'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'col-span-12',
|
||||||
|
hiddenKeys?.includes('schema') ? 'hidden' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={schemaOptions}
|
||||||
|
value={(localValue as any).schema ?? (localValue as any).dataset}
|
||||||
|
disabled={!schemaOptions || disabledKeys?.includes('schema')}
|
||||||
|
onChange={e => {
|
||||||
|
e.persist();
|
||||||
|
const temp = {
|
||||||
|
...localValue,
|
||||||
|
schema: e.target.value,
|
||||||
|
table: '',
|
||||||
|
};
|
||||||
|
setLocalValue(temp);
|
||||||
|
updateParent(temp);
|
||||||
|
}}
|
||||||
|
placeholder="Select a schema..."
|
||||||
|
error={maybeError?.schema}
|
||||||
|
data-testid={`${name}_schema`}
|
||||||
|
icon={<FaFolder />}
|
||||||
|
label={
|
||||||
|
(tree[localValue.database]?.kind === 'bigquery'
|
||||||
|
? labels?.dataset
|
||||||
|
: labels?.schema) ?? 'Schema'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'col-span-12',
|
||||||
|
hiddenKeys?.includes('table') ? 'hidden' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={tableOptions}
|
||||||
|
value={localValue.table}
|
||||||
|
disabled={!tableOptions || disabledKeys?.includes('table')}
|
||||||
|
onChange={e => {
|
||||||
|
e.persist();
|
||||||
|
const temp = {
|
||||||
|
...localValue,
|
||||||
|
table: e.target.value,
|
||||||
|
};
|
||||||
|
setLocalValue(temp);
|
||||||
|
updateParent(temp);
|
||||||
|
}}
|
||||||
|
placeholder="Select a table..."
|
||||||
|
error={maybeError?.table}
|
||||||
|
data-testid={`${name}_table`}
|
||||||
|
icon={<FaTable />}
|
||||||
|
label={labels?.table || 'Table'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,50 @@
|
|||||||
|
import { FieldWrapper } from '@/new-components/Form';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
import { FieldError } from 'react-hook-form';
|
||||||
|
|
||||||
|
interface TSelect extends React.ComponentProps<'select'> {
|
||||||
|
options: string[];
|
||||||
|
error?: FieldError;
|
||||||
|
icon?: React.ReactElement;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Select = ({
|
||||||
|
options,
|
||||||
|
placeholder,
|
||||||
|
disabled,
|
||||||
|
error,
|
||||||
|
...props
|
||||||
|
}: TSelect) => (
|
||||||
|
<FieldWrapper
|
||||||
|
id={props.name}
|
||||||
|
error={error}
|
||||||
|
labelIcon={props.icon}
|
||||||
|
label={props.label}
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
{...props}
|
||||||
|
className={clsx(
|
||||||
|
'block w-full h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus:outline-0 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400',
|
||||||
|
disabled
|
||||||
|
? 'cursor-not-allowed bg-gray-100 border-gray-100'
|
||||||
|
: 'hover:border-gray-400'
|
||||||
|
)}
|
||||||
|
disabled={disabled}
|
||||||
|
value={props.value}
|
||||||
|
>
|
||||||
|
{placeholder ? (
|
||||||
|
<option disabled value="">
|
||||||
|
{placeholder}
|
||||||
|
</option>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{options.map((op, i) => (
|
||||||
|
<option value={op} key={i}>
|
||||||
|
{op}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</FieldWrapper>
|
||||||
|
);
|
@ -0,0 +1,34 @@
|
|||||||
|
import { useSources } from '@/features/MetadataAPI';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const useSourcesTree = () => {
|
||||||
|
const { data, isLoading } = useSources();
|
||||||
|
|
||||||
|
const tree = useMemo(() => {
|
||||||
|
if (!data) return {};
|
||||||
|
|
||||||
|
const temp = data.reduce((databases, source) => {
|
||||||
|
return {
|
||||||
|
...databases,
|
||||||
|
[source.name]: {
|
||||||
|
kind: source.kind,
|
||||||
|
children: source.tables.reduce((schemas, { table }) => {
|
||||||
|
const key = source.kind === 'bigquery' ? 'dataset' : 'schema';
|
||||||
|
const child = table.schema ?? (table as any).dataset;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...schemas,
|
||||||
|
[child]: source.tables
|
||||||
|
.filter((t: any) => t.table[key] === child)
|
||||||
|
.map(t => t.table.name),
|
||||||
|
};
|
||||||
|
}, {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return temp as any;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return { tree, isLoading };
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export { DatabaseSelector } from './DatabaseSelector';
|
@ -0,0 +1,180 @@
|
|||||||
|
import { rest } from 'msw';
|
||||||
|
|
||||||
|
export const handlers = () => [
|
||||||
|
rest.post(`http://localhost:8080/v1/metadata`, (req, res, ctx) => {
|
||||||
|
return res(
|
||||||
|
ctx.json({
|
||||||
|
resource_version: 30,
|
||||||
|
metadata: {
|
||||||
|
version: 3,
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
name: 'bigquery_test',
|
||||||
|
kind: 'bigquery',
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
dataset: 'sensei',
|
||||||
|
name: 'table1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bikes',
|
||||||
|
kind: 'mssql',
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'production',
|
||||||
|
name: 'brands',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'production',
|
||||||
|
name: 'categories',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'sales',
|
||||||
|
name: 'customers',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'sales',
|
||||||
|
name: 'order_items',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'sales',
|
||||||
|
name: 'orders',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'production',
|
||||||
|
name: 'products',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'sales',
|
||||||
|
name: 'staffs',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'production',
|
||||||
|
name: 'stocks',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'sales',
|
||||||
|
name: 'stores',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'chinook',
|
||||||
|
kind: 'postgres',
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'public',
|
||||||
|
name: 'Album',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'public',
|
||||||
|
name: 'Artist',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'public',
|
||||||
|
name: 'Customer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'public',
|
||||||
|
name: 'Employee',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'public',
|
||||||
|
name: 'Genre',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'public',
|
||||||
|
name: 'Invoice',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'public',
|
||||||
|
name: 'InvoiceLine',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'public',
|
||||||
|
name: 'MediaType',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'public',
|
||||||
|
name: 'Playlist',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'public',
|
||||||
|
name: 'PlaylistTrack',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'public',
|
||||||
|
name: 'Track',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'public',
|
||||||
|
name: 'ax',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
schema: 'public',
|
||||||
|
name: 'ideav',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
configuration: {
|
||||||
|
connection_info: {
|
||||||
|
use_prepared_statements: false,
|
||||||
|
database_url:
|
||||||
|
'postgres://postgres:test@host.docker.internal:6001/chinook',
|
||||||
|
isolation_level: 'read-committed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
];
|
1
console/src/features/Data/components/index.ts
Normal file
1
console/src/features/Data/components/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { DatabaseSelector } from './DatabaseSelector';
|
1
console/src/features/Data/index.ts
Normal file
1
console/src/features/Data/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './components';
|
@ -1,3 +1,4 @@
|
|||||||
export * from './Form';
|
export * from './Form';
|
||||||
export * from './InputField';
|
export * from './InputField';
|
||||||
export * from './Select';
|
export * from './Select';
|
||||||
|
export * from './FieldWrapper';
|
||||||
|
Loading…
Reference in New Issue
Block a user