mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
console: tree nav storybook component
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9225 Co-authored-by: Matthew Goodwin <49927862+m4ttheweric@users.noreply.github.com> GitOrigin-RevId: b4772448e7ebceee49c0b49cd7fcf2091ddf88c9
This commit is contained in:
parent
4c0a7cc46a
commit
03daf994b5
@ -0,0 +1,25 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { NavTree } from './NavTree';
|
||||
import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query';
|
||||
import { handlers } from './mocks/handlers';
|
||||
|
||||
export default {
|
||||
component: NavTree,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
} satisfies Meta<typeof NavTree>;
|
||||
|
||||
type Story = StoryObj<typeof NavTree>;
|
||||
|
||||
export const Primary: Story = {
|
||||
render: () => (
|
||||
<NavTree
|
||||
defaultSelection={{
|
||||
dataSourceName: 'chinook',
|
||||
table: { name: 'Album', schema: 'public' },
|
||||
}}
|
||||
/>
|
||||
),
|
||||
parameters: {
|
||||
msw: handlers(),
|
||||
},
|
||||
};
|
@ -0,0 +1,85 @@
|
||||
import { Input } from '../../../../new-components/Form';
|
||||
import { useState } from 'react';
|
||||
import { FaSearch } from 'react-icons/fa';
|
||||
import {
|
||||
useInconsistentMetadata,
|
||||
useMetadata,
|
||||
} from '../../../../features/hasura-metadata-api';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import {
|
||||
QualifiedFunction,
|
||||
Table,
|
||||
} from '../../../../features/hasura-metadata-types';
|
||||
import { useAvailableDrivers } from '../../../../features/ConnectDB';
|
||||
import { TreeComponent } from './components/TreeComponent';
|
||||
import {
|
||||
adaptInconsistentObjects,
|
||||
adaptSourcesIntoTreeData,
|
||||
} from './selectors';
|
||||
|
||||
type Selection = {
|
||||
dataSourceName: string;
|
||||
} & (
|
||||
| {
|
||||
table: Table;
|
||||
function?: never;
|
||||
}
|
||||
| {
|
||||
table?: never;
|
||||
function: QualifiedFunction;
|
||||
}
|
||||
);
|
||||
|
||||
type Props = {
|
||||
defaultSelection?: Selection;
|
||||
handleTableClick?: (table: Table) => void;
|
||||
handleDatabaseClick?: (dataSourceName: string) => void;
|
||||
};
|
||||
|
||||
export const NavTree = (props: Props) => {
|
||||
const [term, setTerm] = useState('');
|
||||
|
||||
const selection = JSON.stringify(props?.defaultSelection ?? {});
|
||||
|
||||
const { data: drivers = [], isFetching: isDriverInfoFetching } =
|
||||
useAvailableDrivers();
|
||||
|
||||
const {
|
||||
data: inconsistentData = {
|
||||
inconsistentSources: [],
|
||||
inconsistentTables: [],
|
||||
inconsistentFunctions: [],
|
||||
},
|
||||
isFetching,
|
||||
} = useInconsistentMetadata(adaptInconsistentObjects);
|
||||
|
||||
const { data: treeData = [], isLoading } = useMetadata(
|
||||
m => adaptSourcesIntoTreeData(m)(drivers, inconsistentData),
|
||||
{
|
||||
enabled: !isFetching && !isDriverInfoFetching,
|
||||
}
|
||||
);
|
||||
|
||||
if (isLoading) return <Skeleton height={30} count={8} />;
|
||||
|
||||
console.log(treeData, selection);
|
||||
|
||||
return (
|
||||
<div id="tree-stuff">
|
||||
<div>
|
||||
<Input
|
||||
name="search"
|
||||
onChange={e => setTerm(e.target.value)}
|
||||
icon={<FaSearch />}
|
||||
/>
|
||||
</div>
|
||||
<TreeComponent
|
||||
treeData={treeData}
|
||||
onDatabaseClick={data => console.log(data)}
|
||||
onLeafNodeClick={data => console.log(data)}
|
||||
selection={selection}
|
||||
term={term}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,124 @@
|
||||
import { NodeRendererProps } from 'react-arborist';
|
||||
import { DataSourceNode } from '../types';
|
||||
import { FaAngleDown, FaAngleRight, FaDatabase } from 'react-icons/fa';
|
||||
import clsx from 'clsx';
|
||||
import { Tooltip } from '../../../../../new-components/Tooltip';
|
||||
import { Badge } from '../../../../../new-components/Badge';
|
||||
import { GetHighlightedText } from './GetHighlightedText';
|
||||
|
||||
const StatusReport = ({
|
||||
node,
|
||||
tree,
|
||||
style,
|
||||
}: NodeRendererProps<DataSourceNode>) => {
|
||||
const details = {
|
||||
Driver: <div>{node.data.driver}</div>,
|
||||
Release: (
|
||||
<div>
|
||||
<Badge>
|
||||
{node.data.releaseType === 'GA' ? 'Stable' : node.data.releaseType}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
Status: node.data.inconsistentObject ? (
|
||||
<span className="text-red-400">Inconsistent</span>
|
||||
) : (
|
||||
<span className="text-green-400">Active</span>
|
||||
),
|
||||
Errors: node.data.inconsistentObject ? (
|
||||
<div className="bg-white break-words max-h-[320px] max-w-sm overflow-y-scroll p-2 rounded text-black whitespace-pre-line">
|
||||
{JSON.stringify(node.data.inconsistentObject)}
|
||||
</div>
|
||||
) : null,
|
||||
};
|
||||
|
||||
return (
|
||||
<table className="border-separate">
|
||||
<tbody>
|
||||
{Object.entries(details)
|
||||
.filter(([key, value]) => !!value)
|
||||
.map(([key, value]) => {
|
||||
return (
|
||||
<tr className="row">
|
||||
<td className="col flex">
|
||||
<div className="font-bold">{key}</div>
|
||||
</td>
|
||||
<td className="col">{value}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatabaseNodeItem = (props: NodeRendererProps<DataSourceNode>) => {
|
||||
const { node, tree, style } = props;
|
||||
return (
|
||||
<div className="flex gap-0.5 items-center" style={style}>
|
||||
{node.children?.length ? (
|
||||
node.isOpen ? (
|
||||
<FaAngleDown
|
||||
data-testid={`${node.data.name}-close`}
|
||||
className="text-gray-600 hover:cursor-pointer"
|
||||
onClick={() => {
|
||||
node.toggle();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FaAngleRight
|
||||
data-testid={`${node.data.name}-expand`}
|
||||
className="text-gray-600 hover:cursor-pointer"
|
||||
onClick={() => node.toggle()}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<FaAngleRight className="text-gray-400" />
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center py-1.5 hover:rounded hover:bg-gray-200 hover:cursor-pointer',
|
||||
node.isSelected ? 'bg-gray-200 rounded' : ''
|
||||
)}
|
||||
onClick={() => {
|
||||
node.data?.onClick?.();
|
||||
}}
|
||||
>
|
||||
<span className="relative">
|
||||
{node.data.inconsistentObject && (
|
||||
<span
|
||||
id="dot"
|
||||
className="rounded-lg absolute h-[10px] w-[10px] top-0 right-[-5px] bg-red-600"
|
||||
data-testid={`${node.data.name}-error-indicator`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tooltip
|
||||
tooltipContentChildren={<StatusReport {...props} />}
|
||||
side="bottom"
|
||||
align="start"
|
||||
>
|
||||
<FaDatabase className="text-gray-600 text-gray-600 text-lg" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
||||
<span className="ml-sm">
|
||||
<GetHighlightedText
|
||||
text={node.data.name}
|
||||
highlight={tree.searchTerm}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{node.children?.length ? (
|
||||
<span
|
||||
className="bg-gray-300 ml-1.5 mr-1.5 px-1.5 py-0.5 rounded text-xs"
|
||||
data-testId={`${node.data.name}-object-count`}
|
||||
>
|
||||
{node.children?.length}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
import { TbMathFunction } from 'react-icons/tb';
|
||||
import { FunctionNode } from '../types';
|
||||
import clsx from 'clsx';
|
||||
import { NodeRendererProps } from 'react-arborist';
|
||||
import { GetHighlightedText } from './GetHighlightedText';
|
||||
|
||||
export const FunctionNodeItem = ({
|
||||
node,
|
||||
tree,
|
||||
style,
|
||||
}: NodeRendererProps<FunctionNode>) => {
|
||||
return (
|
||||
<div className="flex gap-0.5 items-center" style={style}>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center px-1.5 py-1.5 hover:rounded hover:bg-gray-200 hover:cursor-pointer',
|
||||
node.isSelected ? 'bg-gray-200 rounded' : ''
|
||||
)}
|
||||
onClick={() => {
|
||||
node.data?.onClick?.();
|
||||
}}
|
||||
>
|
||||
<TbMathFunction className="text-gray-600" />
|
||||
|
||||
<span className="ml-1.5">
|
||||
<GetHighlightedText
|
||||
text={node.data.name}
|
||||
highlight={tree.searchTerm}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
export const GetHighlightedText = ({
|
||||
text,
|
||||
highlight,
|
||||
}: {
|
||||
text: string;
|
||||
highlight: string;
|
||||
}) => {
|
||||
// Split text on highlight term, include term itself into parts, ignore case
|
||||
const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
|
||||
return (
|
||||
<div data-testid={text}>
|
||||
{parts.map((part, index) =>
|
||||
part.toLowerCase() === highlight.toLowerCase() ? (
|
||||
<b key={index}>{part}</b>
|
||||
) : (
|
||||
part
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
import { TableNode } from '../types';
|
||||
import clsx from 'clsx';
|
||||
import { NodeRendererProps } from 'react-arborist';
|
||||
import { FaTable } from 'react-icons/fa';
|
||||
import { GetHighlightedText } from './GetHighlightedText';
|
||||
|
||||
export const TableNodeItem = ({
|
||||
node,
|
||||
tree,
|
||||
style,
|
||||
}: NodeRendererProps<TableNode>) => {
|
||||
return (
|
||||
<div className="flex gap-0.5 items-center" style={style}>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center px-1.5 py-1.5 hover:rounded hover:bg-gray-200 hover:cursor-pointer',
|
||||
node.isSelected ? 'bg-gray-200 rounded' : ''
|
||||
)}
|
||||
onClick={() => {
|
||||
node.data?.onClick?.();
|
||||
}}
|
||||
>
|
||||
<FaTable className="text-gray-600" />
|
||||
|
||||
<span className="ml-1.5">
|
||||
<GetHighlightedText
|
||||
text={node.data.name}
|
||||
highlight={tree.searchTerm}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,299 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { TreeComponent } from './TreeComponent';
|
||||
import { ReactQueryDecorator } from '../../../../../storybook/decorators/react-query';
|
||||
import { ReleaseType } from '../../../../../features/DataSource';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
import { expect } from '@storybook/jest';
|
||||
import { hasuraToast } from '../../../../../new-components/Toasts';
|
||||
import { getQualifiedTable } from '../../../../../features/Data/ManageTable/utils';
|
||||
|
||||
const meta: Meta<typeof TreeComponent> = {
|
||||
component: TreeComponent,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof TreeComponent>;
|
||||
|
||||
const mockData = [
|
||||
{
|
||||
id: '{"dataSourceName":"chinook"}',
|
||||
dataSourceName: 'chinook',
|
||||
name: 'chinook',
|
||||
driver: 'postgres',
|
||||
releaseType: 'GA' as ReleaseType,
|
||||
children: [
|
||||
{
|
||||
id: '{"dataSourceName":"chinook","table":{"name":"Album","schema":"public"}}',
|
||||
table: {
|
||||
name: 'Album',
|
||||
schema: 'public',
|
||||
},
|
||||
name: 'public / Album',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"chinook","table":{"name":"Artist","schema":"public"}}',
|
||||
table: {
|
||||
name: 'Artist',
|
||||
schema: 'public',
|
||||
},
|
||||
name: 'public / Artist',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"chinook","table":{"name":"Customer","schema":"public"}}',
|
||||
table: {
|
||||
name: 'Customer',
|
||||
schema: 'public',
|
||||
},
|
||||
name: 'public / Customer',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"chinook","table":{"name":"Employee","schema":"public"}}',
|
||||
table: {
|
||||
name: 'Employee',
|
||||
schema: 'public',
|
||||
},
|
||||
name: 'public / Employee',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"chinook","table":{"name":"Genre","schema":"public"}}',
|
||||
table: {
|
||||
name: 'Genre',
|
||||
schema: 'public',
|
||||
},
|
||||
name: 'public / Genre',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"chinook","table":{"name":"Invoice","schema":"public"}}',
|
||||
table: {
|
||||
name: 'Invoice',
|
||||
schema: 'public',
|
||||
},
|
||||
name: 'public / Invoice',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"chinook","table":{"name":"InvoiceLine","schema":"public"}}',
|
||||
table: {
|
||||
name: 'InvoiceLine',
|
||||
schema: 'public',
|
||||
},
|
||||
name: 'public / InvoiceLine',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"chinook","table":{"name":"MediaType","schema":"public"}}',
|
||||
table: {
|
||||
name: 'MediaType',
|
||||
schema: 'public',
|
||||
},
|
||||
name: 'public / MediaType',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"chinook","table":{"name":"Playlist","schema":"public"}}',
|
||||
table: {
|
||||
name: 'Playlist',
|
||||
schema: 'public',
|
||||
},
|
||||
name: 'public / Playlist',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"chinook","table":{"name":"PlaylistTrack","schema":"public"}}',
|
||||
table: {
|
||||
name: 'PlaylistTrack',
|
||||
schema: 'public',
|
||||
},
|
||||
name: 'public / PlaylistTrack',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"chinook","table":{"name":"Track","schema":"public"}}',
|
||||
table: {
|
||||
name: 'Track',
|
||||
schema: 'public',
|
||||
},
|
||||
name: 'public / Track',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"chinook","function":{"name":"all_albums","schema":"public"}}',
|
||||
function: {
|
||||
name: 'all_albums',
|
||||
schema: 'public',
|
||||
},
|
||||
name: 'public / all_albums',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"chinook","function":{"name":"all_albums_1","schema":"public"}}',
|
||||
function: {
|
||||
name: 'all_albums_1',
|
||||
schema: 'public',
|
||||
},
|
||||
name: 'public / all_albums_1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"snowflake_test"}',
|
||||
dataSourceName: 'snowflake_test',
|
||||
name: 'snowflake_test',
|
||||
driver: 'snowflake',
|
||||
releaseType: 'GA' as ReleaseType,
|
||||
inconsistentObject: {
|
||||
definition: 'snowflake_test',
|
||||
name: 'source snowflake_test',
|
||||
reason:
|
||||
'Inconsistent object: Data connector named "snowflake" was not found in the data connector backend info',
|
||||
type: 'source',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: '{"dataSourceName":"snowflake_test","table":["ALBUM"]}',
|
||||
table: ['ALBUM'],
|
||||
name: 'ALBUM',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"snowflake_test","table":["ARTIST"]}',
|
||||
table: ['ARTIST'],
|
||||
name: 'ARTIST',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"snowflake_test","table":["CUSTOMER"]}',
|
||||
table: ['CUSTOMER'],
|
||||
name: 'CUSTOMER',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"snowflake_test","table":["EMPLOYEE"]}',
|
||||
table: ['EMPLOYEE'],
|
||||
name: 'EMPLOYEE',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"snowflake_test","table":["GENRE"]}',
|
||||
table: ['GENRE'],
|
||||
name: 'GENRE',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"snowflake_test","table":["INVOICE"]}',
|
||||
table: ['INVOICE'],
|
||||
name: 'INVOICE',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"snowflake_test","table":["INVOICELINE"]}',
|
||||
table: ['INVOICELINE'],
|
||||
name: 'INVOICELINE',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"snowflake_test","table":["MEDIATYPE"]}',
|
||||
table: ['MEDIATYPE'],
|
||||
name: 'MEDIATYPE',
|
||||
},
|
||||
{
|
||||
id: '{"dataSourceName":"snowflake_test","table":["PLAYLIST"]}',
|
||||
table: ['PLAYLIST'],
|
||||
name: 'PLAYLIST',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const Primary: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<TreeComponent
|
||||
treeData={mockData}
|
||||
onDatabaseClick={dataSourceName =>
|
||||
hasuraToast({
|
||||
title: `${dataSourceName} was clicked`,
|
||||
})
|
||||
}
|
||||
onLeafNodeClick={data => {
|
||||
const qualifiedName =
|
||||
'table' in data
|
||||
? getQualifiedTable(data.table)
|
||||
: getQualifiedTable(data.function);
|
||||
const type = 'table' in data ? 'table' : 'function';
|
||||
hasuraToast({
|
||||
title: `${type} ${data.dataSourceName} / ${qualifiedName.join(
|
||||
' / '
|
||||
)} was clicked`,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// open the database
|
||||
await expect(await canvas.getByTestId('chinook')).toBeVisible();
|
||||
|
||||
// check if the number of tables inside has been rendered properly
|
||||
await expect(
|
||||
await canvas.getByTestId('chinook-object-count')
|
||||
).toHaveTextContent('13');
|
||||
|
||||
// The tree should expand on click
|
||||
await userEvent.click(await canvas.findByTestId('chinook-expand'));
|
||||
|
||||
// this is a surprise tool that will help us later
|
||||
const copyOfAlbumTableReference = await canvas.getByTestId(
|
||||
'public / Album'
|
||||
);
|
||||
|
||||
// check all the tables one-by-one.
|
||||
await expect(await canvas.getByTestId('public / Album')).toBeVisible();
|
||||
|
||||
await expect(await canvas.getByTestId('public / Artist')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('public / Customer')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('public / Employee')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('public / Genre')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('public / Invoice')).toBeVisible();
|
||||
await expect(
|
||||
await canvas.getByTestId('public / InvoiceLine')
|
||||
).toBeVisible();
|
||||
await expect(await canvas.getByTestId('public / MediaType')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('public / Playlist')).toBeVisible();
|
||||
await expect(
|
||||
await canvas.getByTestId('public / PlaylistTrack')
|
||||
).toBeVisible();
|
||||
await expect(await canvas.getByTestId('public / Track')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('public / all_albums')).toBeVisible();
|
||||
await expect(
|
||||
await canvas.getByTestId('public / all_albums_1')
|
||||
).toBeVisible();
|
||||
|
||||
// click on a table and function and check if the callback return the right values
|
||||
await userEvent.click(await canvas.getByTestId('public / Album'));
|
||||
await expect(
|
||||
await canvas.getByText('table chinook / public / Album was clicked')
|
||||
).toBeInTheDocument();
|
||||
await userEvent.click(await canvas.getByTestId('public / all_albums'));
|
||||
await expect(
|
||||
await canvas.getByText(
|
||||
'function chinook / public / all_albums was clicked'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Check the snowflake database
|
||||
await expect(await canvas.getByTestId('snowflake_test')).toBeVisible();
|
||||
await expect(
|
||||
await canvas.getByTestId('snowflake_test-object-count')
|
||||
).toHaveTextContent('9');
|
||||
|
||||
// this one has an inconsistency. A red dot is shown on the DB icon.
|
||||
await expect(
|
||||
await canvas.getByTestId('snowflake_test-error-indicator')
|
||||
).toBeVisible();
|
||||
await userEvent.click(await canvas.findByTestId('snowflake_test-expand'));
|
||||
await expect(await canvas.getByTestId('ALBUM')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('ARTIST')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('CUSTOMER')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('EMPLOYEE')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('GENRE')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('INVOICE')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('INVOICELINE')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('MEDIATYPE')).toBeVisible();
|
||||
await expect(await canvas.getByTestId('PLAYLIST')).toBeVisible();
|
||||
|
||||
// close one of the sources and check if the tables are visible, they shouldn't be
|
||||
await userEvent.click(await canvas.findByTestId('chinook-close'));
|
||||
await expect(copyOfAlbumTableReference).not.toBeVisible();
|
||||
await expect(await canvas.getByTestId('ALBUM')).toBeVisible();
|
||||
},
|
||||
};
|
@ -0,0 +1,92 @@
|
||||
import { NodeRendererProps, Tree } from 'react-arborist';
|
||||
import {
|
||||
DataSourceNode,
|
||||
FunctionNode,
|
||||
TableNode,
|
||||
isDataSourceNode,
|
||||
isFunctionNode,
|
||||
// isTableNode,
|
||||
} from '../types';
|
||||
import { DatabaseNodeItem } from './DatabaseNodeItem';
|
||||
import { FunctionNodeItem } from './FunctionNodeItem';
|
||||
import { TableNodeItem } from './TableNodeItem';
|
||||
import {
|
||||
QualifiedFunction,
|
||||
Table,
|
||||
} from '../../../../../features/hasura-metadata-types';
|
||||
|
||||
function Node(
|
||||
props:
|
||||
| NodeRendererProps<DataSourceNode>
|
||||
| NodeRendererProps<TableNode>
|
||||
| NodeRendererProps<FunctionNode>
|
||||
) {
|
||||
// const nodeData = node.data;
|
||||
|
||||
if (isDataSourceNode(props)) return <DatabaseNodeItem {...props} />;
|
||||
|
||||
if (isFunctionNode(props)) return <FunctionNodeItem {...props} />;
|
||||
|
||||
return <TableNodeItem {...props} />;
|
||||
}
|
||||
|
||||
export const TreeComponent = (props: {
|
||||
treeData: DataSourceNode[];
|
||||
selection?: string;
|
||||
term?: string;
|
||||
onDatabaseClick?: (dataSourceName: string) => void;
|
||||
onLeafNodeClick?: (
|
||||
leaf:
|
||||
| {
|
||||
dataSourceName: string;
|
||||
table: Table;
|
||||
}
|
||||
| {
|
||||
dataSourceName: string;
|
||||
function: QualifiedFunction;
|
||||
}
|
||||
) => void;
|
||||
}) => {
|
||||
const data: DataSourceNode[] = props.treeData.map(source => {
|
||||
return {
|
||||
...source,
|
||||
onClick: () => props.onDatabaseClick?.(source.dataSourceName),
|
||||
children: source.children?.map(leaf => ({
|
||||
...leaf,
|
||||
onClick: () => {
|
||||
if ('table' in leaf)
|
||||
props.onLeafNodeClick?.({
|
||||
dataSourceName: source.dataSourceName,
|
||||
table: leaf.table,
|
||||
});
|
||||
else
|
||||
props.onLeafNodeClick?.({
|
||||
dataSourceName: source.dataSourceName,
|
||||
function: leaf.function,
|
||||
});
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Tree
|
||||
initialData={data}
|
||||
openByDefault={false}
|
||||
width={'inherit'}
|
||||
indent={24}
|
||||
rowHeight={36}
|
||||
height={800}
|
||||
overscanCount={1}
|
||||
searchTerm={props.term}
|
||||
padding={15}
|
||||
selection={props.selection}
|
||||
disableMultiSelection={true}
|
||||
searchMatch={(node, term) =>
|
||||
node.data.name.toLowerCase().includes(term.toLowerCase())
|
||||
}
|
||||
>
|
||||
{Node}
|
||||
</Tree>
|
||||
);
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
import { rest } from 'msw';
|
||||
import {
|
||||
mockInconsistentMetadata,
|
||||
mockListSourceKindsResponse,
|
||||
mockMetadata,
|
||||
} from './mockData';
|
||||
|
||||
const baseUrl = 'http://localhost:8080';
|
||||
|
||||
export const handlers = (url = baseUrl) => [
|
||||
rest.post(`${url}/v1/metadata`, async (_req, res, ctx) => {
|
||||
const reqBody = (await _req.json()) as Record<string, any>;
|
||||
|
||||
if (reqBody.type === 'list_source_kinds')
|
||||
return res(ctx.json(mockListSourceKindsResponse));
|
||||
if (reqBody.type === 'export_metadata') return res(ctx.json(mockMetadata));
|
||||
if (reqBody.type === 'get_inconsistent_metadata')
|
||||
return res(ctx.json(mockInconsistentMetadata));
|
||||
}),
|
||||
];
|
@ -0,0 +1,164 @@
|
||||
import { Metadata } from '../../../../../features/hasura-metadata-types';
|
||||
|
||||
export const mockMetadata: Metadata = {
|
||||
resource_version: 24,
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'bikes',
|
||||
kind: 'mssql',
|
||||
tables: [
|
||||
{ table: { name: 'brands', schema: 'production' } },
|
||||
{ table: { name: 'categories', schema: 'production' } },
|
||||
{ table: { name: 'customers', schema: 'sales' } },
|
||||
{ table: { name: 'order_items', schema: 'sales' } },
|
||||
{ table: { name: 'orders', schema: 'sales' } },
|
||||
{ table: { name: 'products', schema: 'production' } },
|
||||
{ table: { name: 'staffs', schema: 'sales' } },
|
||||
{ table: { name: 'stocks', schema: 'production' } },
|
||||
{ table: { name: 'stores', schema: 'sales' } },
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
connection_string:
|
||||
'DRIVER={ODBC Driver 17 for SQL Server};SERVER=host.docker.internal;DATABASE=bikes;Uid=SA;Pwd=reallyStrongPwd123',
|
||||
pool_settings: {
|
||||
idle_timeout: 5,
|
||||
total_max_connections: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'chinook',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{ table: { name: 'Album', schema: 'public' } },
|
||||
{ table: { name: 'Artist', schema: 'public' } },
|
||||
{ table: { name: 'Customer', schema: 'public' } },
|
||||
{ table: { name: 'Employee', schema: 'public' } },
|
||||
{ table: { name: 'Genre', schema: 'public' } },
|
||||
{ table: { name: 'Invoice', schema: 'public' } },
|
||||
{ table: { name: 'InvoiceLine', schema: 'public' } },
|
||||
{ table: { name: 'MediaType', schema: 'public' } },
|
||||
{ table: { name: 'Playlist', schema: 'public' } },
|
||||
{ table: { name: 'PlaylistTrack', schema: 'public' } },
|
||||
{ table: { name: 'Track', schema: 'public' } },
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url:
|
||||
'postgres://postgres:test@host.docker.internal:6001/chinook',
|
||||
isolation_level: 'read-committed',
|
||||
use_prepared_statements: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'snowflake_test',
|
||||
kind: 'snowflake',
|
||||
tables: [
|
||||
{ table: ['ALBUM'] },
|
||||
{ table: ['ARTIST'] },
|
||||
{ table: ['CUSTOMER'] },
|
||||
{ table: ['EMPLOYEE'] },
|
||||
{ table: ['GENRE'] },
|
||||
{ table: ['INVOICE'] },
|
||||
{ table: ['INVOICELINE'] },
|
||||
{ table: ['MEDIATYPE'] },
|
||||
{ table: ['PLAYLIST'] },
|
||||
{ table: ['PLAYLISTTRACK'] },
|
||||
{ table: ['TRACK'] },
|
||||
],
|
||||
configuration: {
|
||||
template: null,
|
||||
timeout: null,
|
||||
value: {
|
||||
fully_qualify_all_names: false,
|
||||
jdbc_url: 'jdbc_url',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
backend_configs: {
|
||||
dataconnector: {
|
||||
athena: { uri: 'http://host.docker.internal:8081/api/v1/athena' },
|
||||
mariadb: { uri: 'http://host.docker.internal:8081/api/v1/mariadb' },
|
||||
mysql8: { uri: 'http://host.docker.internal:8081/api/v1/mysql' },
|
||||
oracle: { uri: 'http://host.docker.internal:8081/api/v1/oracle' },
|
||||
snowflake: { uri: 'http://host.docker.internal:8081/api/v1/snowflake' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockListSourceKindsResponse = {
|
||||
sources: [
|
||||
{ available: true, builtin: true, display_name: 'pg', kind: 'pg' },
|
||||
{
|
||||
available: true,
|
||||
builtin: true,
|
||||
display_name: 'citus',
|
||||
kind: 'citus',
|
||||
},
|
||||
{
|
||||
available: true,
|
||||
builtin: true,
|
||||
display_name: 'cockroach',
|
||||
kind: 'cockroach',
|
||||
},
|
||||
{
|
||||
available: true,
|
||||
builtin: true,
|
||||
display_name: 'mssql',
|
||||
kind: 'mssql',
|
||||
},
|
||||
{
|
||||
available: true,
|
||||
builtin: true,
|
||||
display_name: 'bigquery',
|
||||
kind: 'bigquery',
|
||||
},
|
||||
{
|
||||
available: true,
|
||||
builtin: false,
|
||||
display_name: 'Amazon Athena',
|
||||
kind: 'athena',
|
||||
release_name: 'Beta',
|
||||
},
|
||||
{
|
||||
available: true,
|
||||
builtin: false,
|
||||
display_name: 'MariaDB',
|
||||
kind: 'mariadb',
|
||||
release_name: 'Beta',
|
||||
},
|
||||
{
|
||||
available: true,
|
||||
builtin: false,
|
||||
display_name: 'MySQL',
|
||||
kind: 'mysql8',
|
||||
release_name: 'Beta',
|
||||
},
|
||||
{
|
||||
available: true,
|
||||
builtin: false,
|
||||
display_name: 'Oracle',
|
||||
kind: 'oracle',
|
||||
release_name: 'Beta',
|
||||
},
|
||||
{
|
||||
available: true,
|
||||
builtin: false,
|
||||
display_name: 'Snowflake',
|
||||
kind: 'snowflake',
|
||||
release_name: 'Beta',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const mockInconsistentMetadata = {
|
||||
inconsistent_objects: [],
|
||||
is_consistent: true,
|
||||
};
|
@ -0,0 +1,67 @@
|
||||
import { getQualifiedTable } from '../../../../features/Data/ManageTable/utils';
|
||||
import { DriverInfo } from '../../../../features/DataSource';
|
||||
import {
|
||||
InconsistentMetadata,
|
||||
InconsistentObject,
|
||||
} from '../../../../features/hasura-metadata-api';
|
||||
import { Metadata } from '../../../../features/hasura-metadata-types';
|
||||
import { DataSourceNode } from './types';
|
||||
|
||||
type InconsistentData = {
|
||||
inconsistentSources: InconsistentObject[];
|
||||
inconsistentTables: InconsistentObject[];
|
||||
inconsistentFunctions: InconsistentObject[];
|
||||
};
|
||||
|
||||
export const adaptInconsistentObjects = (m: InconsistentMetadata) =>
|
||||
m.inconsistent_objects.reduce<InconsistentData>(
|
||||
(acc, entry) => {
|
||||
if (entry.type === 'source') acc.inconsistentSources.push(entry);
|
||||
|
||||
if (entry.type === 'table') acc.inconsistentTables.push(entry);
|
||||
|
||||
if (entry.type === 'function') acc.inconsistentFunctions.push(entry);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
inconsistentSources: [],
|
||||
inconsistentTables: [],
|
||||
inconsistentFunctions: [],
|
||||
}
|
||||
);
|
||||
|
||||
export const adaptSourcesIntoTreeData =
|
||||
(m: Metadata) =>
|
||||
(drivers: DriverInfo[], inconsistentData: InconsistentData) =>
|
||||
m.metadata.sources.map<DataSourceNode>(source => {
|
||||
return {
|
||||
id: JSON.stringify({ dataSourceName: source.name }),
|
||||
dataSourceName: source.name,
|
||||
name: source.name,
|
||||
driver: source.kind,
|
||||
releaseType: drivers?.find(driver => source.kind === driver.name)
|
||||
?.release,
|
||||
inconsistentObject: inconsistentData.inconsistentSources.find(
|
||||
i => i.definition === source.name
|
||||
),
|
||||
children: [
|
||||
...source.tables.map(t => ({
|
||||
id: JSON.stringify({
|
||||
dataSourceName: source.name,
|
||||
table: t.table,
|
||||
}),
|
||||
table: t.table,
|
||||
name: getQualifiedTable(t.table).join(' / '),
|
||||
})),
|
||||
...(source.functions ?? []).map(f => ({
|
||||
id: JSON.stringify({
|
||||
dataSourceName: source.name,
|
||||
function: f.function,
|
||||
}),
|
||||
function: f.function,
|
||||
name: getQualifiedTable(f.function).join(' / '),
|
||||
})),
|
||||
],
|
||||
};
|
||||
});
|
@ -0,0 +1,62 @@
|
||||
import { NodeRendererProps } from 'react-arborist';
|
||||
import {
|
||||
QualifiedFunction,
|
||||
Table,
|
||||
} from '../../../../features/hasura-metadata-types';
|
||||
import { InconsistentObject } from '../../../../features/hasura-metadata-api';
|
||||
import { ReleaseType } from '../../../../features/DataSource';
|
||||
|
||||
export type TableNode = {
|
||||
id: string;
|
||||
table: Table;
|
||||
onClick?: () => void;
|
||||
// needed for search
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type FunctionNode = {
|
||||
id: string;
|
||||
function: QualifiedFunction;
|
||||
onClick?: () => void;
|
||||
// needed for search
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type DataSourceNode = {
|
||||
id: string;
|
||||
dataSourceName: string;
|
||||
driver: string;
|
||||
releaseType?: ReleaseType;
|
||||
onClick?: () => void;
|
||||
children?: (TableNode | FunctionNode)[];
|
||||
inconsistentObject?: InconsistentObject;
|
||||
// needed for search
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const isDataSourceNode = (
|
||||
value:
|
||||
| NodeRendererProps<TableNode>
|
||||
| NodeRendererProps<DataSourceNode>
|
||||
| NodeRendererProps<FunctionNode>
|
||||
): value is NodeRendererProps<DataSourceNode> => {
|
||||
return 'dataSourceName' in value.node.data;
|
||||
};
|
||||
|
||||
export const isFunctionNode = (
|
||||
value:
|
||||
| NodeRendererProps<TableNode>
|
||||
| NodeRendererProps<DataSourceNode>
|
||||
| NodeRendererProps<FunctionNode>
|
||||
): value is NodeRendererProps<FunctionNode> => {
|
||||
return 'function' in value.node.data;
|
||||
};
|
||||
|
||||
export const isTableNode = (
|
||||
value:
|
||||
| NodeRendererProps<TableNode>
|
||||
| NodeRendererProps<DataSourceNode>
|
||||
| NodeRendererProps<FunctionNode>
|
||||
): value is NodeRendererProps<TableNode> => {
|
||||
return 'table' in value.node.data;
|
||||
};
|
@ -0,0 +1,79 @@
|
||||
import { Table } from '../../../../features/hasura-metadata-types';
|
||||
|
||||
export const adaptTableObject = (table: Table): string[] => {
|
||||
if (Array.isArray(table)) return table;
|
||||
|
||||
// This is a safe assumption to make because the only native database that supports functions is postgres( and variants)
|
||||
if (typeof table === 'string') return ['public', table];
|
||||
|
||||
const postgresOrMssqlTable = table as {
|
||||
schema: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
if ('schema' in postgresOrMssqlTable)
|
||||
return [postgresOrMssqlTable.schema, postgresOrMssqlTable.name];
|
||||
|
||||
const bigQueryTable = table as { dataset: string; name: string };
|
||||
|
||||
if ('dataset' in bigQueryTable)
|
||||
return [bigQueryTable.dataset, bigQueryTable.name];
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export function convertToTreeDataForGDCSource(
|
||||
tables: string[][],
|
||||
key: string[],
|
||||
dataSourceName: string,
|
||||
tableLevelClick: (t: Table) => void
|
||||
): any {
|
||||
console.log(tables);
|
||||
if (tables.length === 0) return [];
|
||||
|
||||
if (tables[0].length === 1) {
|
||||
const leafNodes: any = tables.map(table => {
|
||||
return {
|
||||
id: JSON.stringify({
|
||||
database: dataSourceName,
|
||||
table: [...key, table[0]],
|
||||
}),
|
||||
title: table[0],
|
||||
onTableSelect: tableLevelClick,
|
||||
isTable: true,
|
||||
};
|
||||
});
|
||||
|
||||
return leafNodes;
|
||||
}
|
||||
|
||||
const uniqueLevelValues = Array.from(new Set(tables.map(table => table[0])));
|
||||
|
||||
const acc: any = [];
|
||||
|
||||
const values = uniqueLevelValues.reduce((_acc, levelValue) => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const _childTables = tables
|
||||
.filter(table => table[0] === levelValue)
|
||||
.map<string[]>(table => {
|
||||
console.log(table);
|
||||
return table.slice(1);
|
||||
});
|
||||
|
||||
return [
|
||||
..._acc,
|
||||
{
|
||||
id: JSON.stringify([...key, levelValue[0]]),
|
||||
title: levelValue,
|
||||
children: convertToTreeDataForGDCSource(
|
||||
_childTables,
|
||||
[...key, levelValue],
|
||||
dataSourceName,
|
||||
tableLevelClick
|
||||
),
|
||||
},
|
||||
];
|
||||
}, acc);
|
||||
|
||||
return values;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { expect } from '@storybook/jest';
|
||||
import { StoryObj } from '@storybook/react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { screen, userEvent, within } from '@storybook/testing-library';
|
||||
import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query';
|
||||
import { dismissToast } from '../../../../utils/StoryUtils';
|
||||
@ -19,7 +19,7 @@ export default {
|
||||
}),
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
};
|
||||
} satisfies Meta<typeof AddNativeQuery>;
|
||||
|
||||
const fillAndSubmitForm: Story['play'] = async ({ canvasElement }) => {
|
||||
const c = within(canvasElement);
|
||||
|
@ -59,7 +59,7 @@ export const AddNativeQuery = ({
|
||||
s => s.name === selectedSource
|
||||
)?.logical_models;
|
||||
|
||||
const { trackNativeQuery } = useTrackNativeQuery();
|
||||
const { trackNativeQuery, isLoading } = useTrackNativeQuery();
|
||||
|
||||
const [isLogicalModelsDialogOpen, setIsLogicalModelsDialogOpen] =
|
||||
React.useState(false);
|
||||
@ -208,7 +208,12 @@ export const AddNativeQuery = ({
|
||||
Slack thread: https://hasurahq.slack.com/archives/C04LV93JNSH/p1682965503376129
|
||||
*/}
|
||||
{/* <Button icon={<FaPlay />}>Validate</Button> */}
|
||||
<Button type="submit" icon={<FaSave />} mode="primary">
|
||||
<Button
|
||||
type="submit"
|
||||
icon={<FaSave />}
|
||||
mode="primary"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@ import { Metadata } from '../hasura-metadata-types';
|
||||
import { useHttpClient } from '../Network';
|
||||
import { useCallback } from 'react';
|
||||
import { useQuery, useQueryClient } from 'react-query';
|
||||
import { APIError } from '../../hooks/error';
|
||||
|
||||
export const DEFAULT_STALE_TIME = 5 * 60000; // 5 minutes as default stale time
|
||||
|
||||
@ -24,22 +25,31 @@ export const useInvalidateMetadata = () => {
|
||||
return invalidate;
|
||||
};
|
||||
|
||||
export const useMetadata = <T = Metadata>(
|
||||
selector?: (m: Metadata) => T,
|
||||
staleTime: number = DEFAULT_STALE_TIME
|
||||
type Options = {
|
||||
staleTime?: number;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export const useMetadata = <FinalResult = Metadata>(
|
||||
selector?: (m: Metadata) => FinalResult,
|
||||
options: Options = {
|
||||
staleTime: DEFAULT_STALE_TIME,
|
||||
enabled: true,
|
||||
}
|
||||
) => {
|
||||
const httpClient = useHttpClient();
|
||||
const invalidateMetadata = useInvalidateMetadata();
|
||||
|
||||
const queryReturn = useQuery({
|
||||
const queryReturn = useQuery<Metadata, APIError, FinalResult>({
|
||||
queryKey: [METADATA_QUERY_KEY],
|
||||
queryFn: async () => {
|
||||
const result = await exportMetadata({ httpClient });
|
||||
return result;
|
||||
},
|
||||
staleTime: staleTime || DEFAULT_STALE_TIME,
|
||||
staleTime: options.staleTime,
|
||||
refetchOnWindowFocus: false,
|
||||
select: selector,
|
||||
enabled: options.enabled,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -1,16 +1,14 @@
|
||||
import React from 'react';
|
||||
import { DecoratorFn } from '@storybook/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
import { RootState } from '../../store';
|
||||
import { Provider } from 'react-redux';
|
||||
import reducer from '../../reducer';
|
||||
import { RootState } from '../../store';
|
||||
import { DeepPartial } from '../../types';
|
||||
import { Decorator } from '@storybook/react';
|
||||
|
||||
export const ReduxDecorator = (
|
||||
mockValues: DeepPartial<RootState>
|
||||
): DecoratorFn => {
|
||||
): Decorator => {
|
||||
// Dispatch manually inside reducer with undefined as state and a action that shouldn't be used
|
||||
// So we have the base state of the app
|
||||
const baseState = reducer(undefined, { type: '@@INIT_MOCK_STORYBOOK@@' });
|
||||
|
@ -112,6 +112,7 @@
|
||||
"react": "17.0.2",
|
||||
"react-ace": "8.0.0",
|
||||
"react-apollo": "^3.0.1",
|
||||
"react-arborist": "^3.0.2",
|
||||
"react-autosize-textarea": "7.1.0",
|
||||
"react-autosuggest": "10.0.2",
|
||||
"react-bootstrap": "0.32.4",
|
||||
|
@ -6292,6 +6292,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-dnd/asap@npm:^4.0.0":
|
||||
version: 4.0.1
|
||||
resolution: "@react-dnd/asap@npm:4.0.1"
|
||||
checksum: 757db3b5c436a95383b74f187f503321092909401ce9665d9cc1999308a44de22809bf8dbe82c9126bd73b72dd6665bbc4a788e864fc3c243c59f65057a4f87f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-dnd/invariant@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@react-dnd/invariant@npm:2.0.0"
|
||||
checksum: ef1e989920d70b15c80dccb01af9b598081d76993311aa22d2e9a3ec41d10a88540eeec4b4de7a8b2a2ea52dfc3495ab45e39192c2d27795a9258bd6b79d000e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-dnd/shallowequal@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@react-dnd/shallowequal@npm:2.0.0"
|
||||
checksum: b5bbdc795d65945bb7ba2322bed5cf8d4c6fe91dced98c3b10e3d16822c438f558751135ff296f8d1aa1eaa9d0037dacab2b522ca5eb812175123b9996966dcb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@reduxjs/toolkit@npm:^1.5.1":
|
||||
version: 1.9.5
|
||||
resolution: "@reduxjs/toolkit@npm:1.9.5"
|
||||
@ -16227,6 +16248,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dnd-core@npm:14.0.1":
|
||||
version: 14.0.1
|
||||
resolution: "dnd-core@npm:14.0.1"
|
||||
dependencies:
|
||||
"@react-dnd/asap": ^4.0.0
|
||||
"@react-dnd/invariant": ^2.0.0
|
||||
redux: ^4.1.1
|
||||
checksum: dbc50727f53baad1cb1e0430a2a1b81c5c291389322f90fdc46edeb2fd49cc206ce4fa30a95afb53d88238945228e34866d7465f7ea49285c296baf883551301
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dns-equal@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "dns-equal@npm:1.0.0"
|
||||
@ -18855,6 +18887,7 @@ __metadata:
|
||||
react-a11y: 0.2.8
|
||||
react-ace: 8.0.0
|
||||
react-apollo: ^3.0.1
|
||||
react-arborist: ^3.0.2
|
||||
react-autosize-textarea: 7.1.0
|
||||
react-autosuggest: 10.0.2
|
||||
react-bootstrap: 0.32.4
|
||||
@ -29159,6 +29192,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-arborist@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "react-arborist@npm:3.0.2"
|
||||
dependencies:
|
||||
react-dnd: ^14.0.3
|
||||
react-dnd-html5-backend: ^14.0.1
|
||||
react-window: ^1.8.6
|
||||
redux: ^4.1.1
|
||||
use-sync-external-store: ^1.2.0
|
||||
peerDependencies:
|
||||
react: ">= 16.14"
|
||||
react-dom: ">= 16.14"
|
||||
checksum: 8fa035db59f3d6adcbf06e0d140cb5a483edb5f55d9db2f2a13e691acb7ba0a45a89d7c24953756b9f2c263d629dbdf1ed1c5d0a116de5587f6abf0c12ecbc3a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-autosize-textarea@npm:7.1.0":
|
||||
version: 7.1.0
|
||||
resolution: "react-autosize-textarea@npm:7.1.0"
|
||||
@ -29320,6 +29369,40 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-dnd-html5-backend@npm:^14.0.1":
|
||||
version: 14.1.0
|
||||
resolution: "react-dnd-html5-backend@npm:14.1.0"
|
||||
dependencies:
|
||||
dnd-core: 14.0.1
|
||||
checksum: 6aa8d62c6b2288893b3f216d476d2f84495b40d33578ba9e3a5051dc093a71dc59700e6927ed7ac596ff8d7aa3b3f29404f7d173f844bd6144ed633403dd8e96
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-dnd@npm:^14.0.3":
|
||||
version: 14.0.5
|
||||
resolution: "react-dnd@npm:14.0.5"
|
||||
dependencies:
|
||||
"@react-dnd/invariant": ^2.0.0
|
||||
"@react-dnd/shallowequal": ^2.0.0
|
||||
dnd-core: 14.0.1
|
||||
fast-deep-equal: ^3.1.3
|
||||
hoist-non-react-statics: ^3.3.2
|
||||
peerDependencies:
|
||||
"@types/hoist-non-react-statics": ">= 3.3.1"
|
||||
"@types/node": ">= 12"
|
||||
"@types/react": ">= 16"
|
||||
react: ">= 16.14"
|
||||
peerDependenciesMeta:
|
||||
"@types/hoist-non-react-statics":
|
||||
optional: true
|
||||
"@types/node":
|
||||
optional: true
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 464e231de8c2b79546049a1600b67b1df0b7f762f23c688d3e9aeddbf334b1e64931ef91d0129df3c8be255f0af76e89426729dbcbefe4bdc09b0f665d2da368
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-docgen-typescript@npm:^2.2.2":
|
||||
version: 2.2.2
|
||||
resolution: "react-docgen-typescript@npm:2.2.2"
|
||||
@ -30047,7 +30130,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-window@npm:^1":
|
||||
"react-window@npm:^1, react-window@npm:^1.8.6":
|
||||
version: 1.8.9
|
||||
resolution: "react-window@npm:1.8.9"
|
||||
dependencies:
|
||||
@ -30348,7 +30431,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"redux@npm:^4.0.0, redux@npm:^4.0.4, redux@npm:^4.0.5, redux@npm:^4.2.1":
|
||||
"redux@npm:^4.0.0, redux@npm:^4.0.4, redux@npm:^4.0.5, redux@npm:^4.1.1, redux@npm:^4.2.1":
|
||||
version: 4.2.1
|
||||
resolution: "redux@npm:4.2.1"
|
||||
dependencies:
|
||||
|
Loading…
Reference in New Issue
Block a user