client/app/components/queries/SchemaBrowser.jsx (317 lines of code) (raw):

import { isNil, map, filter, some, includes } from "lodash"; import cx from "classnames"; import { axios } from "@/services/axios"; import React, { useState, useCallback, useMemo, useEffect } from "react"; import PropTypes from "prop-types"; import { useDebouncedCallback } from "use-debounce"; import Checkbox from "antd/lib/checkbox"; import Input from "antd/lib/input"; import Button from "antd/lib/button"; import Tooltip from "antd/lib/tooltip"; import AutoSizer from "react-virtualized/dist/commonjs/AutoSizer"; import List from "react-virtualized/dist/commonjs/List"; import useDataSourceSchema from "@/pages/queries/hooks/useDataSourceSchema"; import useImmutableCallback from "@/lib/hooks/useImmutableCallback"; import LoadingState from "../items-list/components/LoadingState"; import { clientConfig } from "@/services/auth"; import notification from "@/services/notification"; import SchemaData from "@/components/queries/SchemaData"; const SchemaItemType = PropTypes.shape({ name: PropTypes.string.isRequired, size: PropTypes.number, columns: PropTypes.arrayOf(PropTypes.object).isRequired, }); const schemaTableHeight = 22; const schemaColumnHeight = 18; function SchemaItem({ item, expanded, onToggle, onSelect, onShowSchema, ...props }) { const handleSelect = useCallback( (event, ...args) => { event.preventDefault(); event.stopPropagation(); onSelect(...args); }, [onSelect] ); const handleShowSchema = useCallback( (event, ...args) => { event.preventDefault(); event.stopPropagation(); onShowSchema(...args); }, [onShowSchema] ); if (!item) { return null; } return ( <div {...props}> <div className="table-name" onClick={onToggle}> <i className="fa fa-table m-r-5" /> <strong> <span title={item.name}>{item.name}</span> {!isNil(item.size) && <span> ({item.size})</span>} </strong> {item.column_metadata && <i className="fa fa-question-circle info" title="More Info" aria-hidden="true" onClick={e => handleShowSchema(e, item)} />} <i className="fa fa-angle-double-right copy-to-editor" aria-hidden="true" onClick={e => handleSelect(e, item.name)} /> </div> {expanded && ( <div> {map(item.columns, column => ( <div key={column.id} className="table-open"> {column.name} <i className="fa fa-angle-double-right copy-to-editor" aria-hidden="true" onClick={e => handleSelect(e, column.name)} /> </div> ))} </div> )} </div> ); } SchemaItem.propTypes = { item: SchemaItemType, expanded: PropTypes.bool, onToggle: PropTypes.func, onSelect: PropTypes.func, }; SchemaItem.defaultProps = { item: null, expanded: false, onToggle: () => {}, onSelect: () => {}, }; function SchemaLoadingState() { return ( <div className="schema-loading-state"> <LoadingState className="" /> </div> ); } export function SchemaList({ loading, schema, expandedFlags, onTableExpand, onItemSelect, openSchemaInfo, closeSchemaInfo }) { const [listRef, setListRef] = useState(null); useEffect(() => { if (listRef) { listRef.recomputeRowHeights(); } }, [listRef, schema, expandedFlags]); return ( <div className="schema-browser"> {loading && <SchemaLoadingState />} {!loading && ( <AutoSizer> {({ width, height }) => ( <List ref={setListRef} width={width} height={height} rowCount={schema.length} rowHeight={({ index }) => { const item = schema[index]; const columnCount = expandedFlags[item.name] ? item.columns.length : 0; return schemaTableHeight + schemaColumnHeight * columnCount; }} rowRenderer={({ key, index, style }) => { const item = schema[index]; return ( <SchemaItem key={key} style={style} item={item} expanded={expandedFlags[item.name]} onToggle={() => onTableExpand(item.name)} onSelect={onItemSelect} onShowSchema={openSchemaInfo} /> ); }} /> )} </AutoSizer> )} </div> ); } function itemExists(item) { if ("visible" in item) { return item.visible; } else { return false; } }; export function applyFilterOnSchema(schema, filterString, showHidden, toggleString) { const filters = filter(filterString.toLowerCase().split(/\s+/), s => s.length > 0); // Filter out extra schema that match the provided toggle string if (!showHidden && toggleString) { const toggleStringRegex = new RegExp(toggleString); try { schema = filter( schema, item => !item.name.toLowerCase().match(toggleStringRegex) ); } catch (err) { notification.error(`Error while matching schema items: ${err}`); } } // Filter out all columns set to invisible schema = filter(schema, itemExists); // Empty string: return original schema if (filters.length === 0) { return schema; } // Single word: matches table or column if (filters.length === 1) { const nameFilter = filters[0]; const columnFilter = filters[0]; return filter( schema, item => includes(item.name.toLowerCase(), nameFilter) || some(item.columns, column => includes(column.name.toLowerCase(), columnFilter)) ); } // Two (or more) words: first matches table, seconds matches column const nameFilter = filters[0]; const columnFilter = filters[1]; return filter( map(schema, item => { if (includes(item.name.toLowerCase(), nameFilter)) { item = { ...item, columns: filter(item.columns, column => includes(column.name.toLowerCase(), columnFilter)) }; return item.columns.length > 0 ? item : null; } }) ); } export default function SchemaBrowser({ dataSource, onSchemaUpdate, onItemSelect, options, onOptionsUpdate, ...props }) { const [schema, isLoading, refreshSchema] = useDataSourceSchema(dataSource); const [filterString, setFilterString] = useState(""); const [showHidden, setShowHidden] = useState(false); const toggleString = useToggleString(dataSource ? dataSource.id : undefined); const filteredSchema = useMemo(() => applyFilterOnSchema(schema, filterString, showHidden, toggleString), [schema, filterString, showHidden, toggleString]); const [handleFilterChange] = useDebouncedCallback(setFilterString, 500); const [handleToggleChange] = useDebouncedCallback(setShowHidden, 100); const [expandedFlags, setExpandedFlags] = useState({}); const [showSchemaInfo, setShowSchemaInfo] = useState(false); const [tableName, setTableName] = useState(""); const [tableDescription, setTableDescription] = useState(""); const [tableMetadata, setTableMetadata] = useState([]); const [sampleQueries, setSampleQueries] = useState([]); const handleSchemaUpdate = useImmutableCallback(onSchemaUpdate); useEffect(() => { setExpandedFlags({}); }, [schema]); useEffect(() => { setExpandedFlags({}); handleSchemaUpdate(schema); }, [schema, handleSchemaUpdate]); if (schema.length === 0 && !isLoading) { return null; } function toggleTable(tableName) { setExpandedFlags({ ...expandedFlags, [tableName]: !expandedFlags[tableName], }); } function useToggleString(dataSourceId) { const [toggleString, setToggleString] = useState(""); useMemo(() => { if (!dataSourceId) { return null; } axios.get( `${clientConfig.basePath}api/data_sources/${dataSourceId}/toggle_string` ).then(data => { setToggleString(data.toggle_string); }) }, [dataSourceId]); return toggleString; } function openSchemaInfo(table) { setTableName(table.name); setTableDescription(table.description); setTableMetadata(table.columns); setSampleQueries(Object.values(table.sample_queries)); setShowSchemaInfo(true); } function closeSchemaInfo() { setShowSchemaInfo(false); }; return ( <div className="schema-container" {...props}> <div className="schema-control"> <Input className="m-r-5" placeholder="Search schema..." disabled={schema.length === 0} onChange={event => handleFilterChange(event.target.value)} /> <Tooltip title="Refresh Schema"> <Button onClick={() => refreshSchema(true)}> <i className={cx("zmdi zmdi-refresh", { "zmdi-hc-spin": isLoading })} /> </Button> </Tooltip> </div> <div> {toggleString && <Tooltip placement="right" title={`Matching pattern: ${toggleString}`}> <Checkbox className="m-t-10" checked={showHidden} onChange={event => handleToggleChange(event.target.checked)}> Show hidden schema </Checkbox></Tooltip>} </div> <SchemaList loading={isLoading && schema.length === 0} schema={filteredSchema} expandedFlags={expandedFlags} onTableExpand={toggleTable} onItemSelect={onItemSelect} toggleString={toggleString} openSchemaInfo={openSchemaInfo} closeSchemaInfo={closeSchemaInfo} /> <SchemaData show={showSchemaInfo} tableName={tableName} tableDescription={tableDescription} tableMetadata={tableMetadata} sampleQueries={sampleQueries} onClose={closeSchemaInfo} /> </div> ); } SchemaBrowser.propTypes = { dataSource: PropTypes.object, // eslint-disable-line react/forbid-prop-types onSchemaUpdate: PropTypes.func, schema: PropTypes.arrayOf(SchemaItemType), onRefresh: PropTypes.func, onItemSelect: PropTypes.func, }; SchemaBrowser.defaultProps = { dataSource: null, onSchemaUpdate: () => {}, schema: [], onRefresh: () => {}, onItemSelect: () => {}, };