UNPKG

datajunction-ui

Version:

DataJunction Metrics Platform UI

368 lines (348 loc) 12.7 kB
import React, { useEffect, useState } from 'react'; import Select from 'react-select'; import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; import { foundation } from 'react-syntax-highlighter/src/styles/hljs'; import { labelize } from '../../../utils/form'; import { Form, Formik } from 'formik'; import DimensionFilter from './DimensionFilter'; import QueryInfo from '../../components/QueryInfo'; import LoadingIcon from '../../icons/LoadingIcon'; export default function NodeValidateTab({ node, djClient }) { const [query, setQuery] = useState(''); const [lookup, setLookup] = useState([]); const [running, setRunning] = useState(false); // These are a list of dimensions that are available for this node const [dimensions, setDimensions] = useState([]); // A separate structure used to store the selected dimensions to filter by and their values const [selectedFilters, setSelectedFilters] = useState({}); // The set of dimensions and filters to pass to the /sql or /data endpoints for the node const [selection, setSelection] = useState({ dimensions: [], filters: [], }); // Any query result info retrieved when a node query is run const [queryInfo, setQueryInfo] = useState(null); const initialValues = {}; const [state, setState] = useState({ selectedTab: 'results', }); const switchTab = tabName => { setState({ selectedTab: tabName }); }; useEffect(() => { const fetchData = async () => { if (node) { // Find all the dimensions for this node const dimensions = await djClient.nodeDimensions(node.name); // Create a dimensions lookup object const lookup = {}; dimensions.forEach(dimension => { lookup[dimension.name] = dimension; }); setLookup(lookup); // Group the dimensions by dimension node const grouped = Object.entries( dimensions.reduce((group, dimension) => { group[dimension.node_name + dimension.path] = group[dimension.node_name + dimension.path] ?? []; group[dimension.node_name + dimension.path].push(dimension); return group; }, {}), ); setDimensions(grouped); // Build the query for this node based on the user-provided dimensions and filters const query = await djClient.sql(node.name, selection); setQuery(query.sql); } }; fetchData().catch(console.error); }, [djClient, node, selection]); const dimensionsList = dimensions.flatMap(grouping => { const dimensionsInGroup = grouping[1]; return dimensionsInGroup .filter(dim => dim.properties.includes('primary_key') === true) .map(dim => { return { value: dim.name, label: ( <span> {labelize(dim.name.split('.').slice(-1)[0])}{' '} <small>{dim.name}</small> </span> ), }; }); }); // Run the query and use SSE to stream the status of the query execution results const runQuery = async (values, setStatus, setSubmitting) => { setRunning(true); const sse = await djClient.streamNodeData(node?.name, selection); sse.onmessage = e => { const messageData = JSON.parse(JSON.parse(e.data)); if ( messageData !== null && messageData?.state !== 'FINISHED' && messageData?.state !== 'CANCELED' && messageData?.state !== 'FAILED' ) { setRunning(false); } if (messageData.results && messageData.results?.length > 0) { messageData.numRows = messageData.results?.length ? messageData.results[0].rows.length : []; switchTab('results'); setRunning(false); } else { switchTab('info'); } setQueryInfo(messageData); }; sse.onerror = () => sse.close(); }; // Handle form submission (runs the query) const handleSubmit = async (values, { setSubmitting, setStatus }) => { await runQuery(values, setStatus, setSubmitting); }; // Handle when filter values are updated. This is available for all nodes. const handleAddFilters = event => { const updatedFilters = selectedFilters; if (event.dimension in updatedFilters) { updatedFilters[event.dimension].operator = event.operator; updatedFilters[event.dimension].values = event.values; } else { updatedFilters[event.dimension] = { operator: event.operator, values: event.values, }; } setSelectedFilters(updatedFilters); const updatedDimensions = selection.dimensions.concat([event.dimension]); setSelection({ filters: Object.entries(updatedFilters).map(obj => obj[1].values ? `${obj[0]} IN (${obj[1].values .map(val => ['int', 'bigint', 'float', 'double', 'long'].includes( lookup[obj[0]].type, ) ? val.value : "'" + val.value + "'", ) .join(', ')})` : '', ), dimensions: updatedDimensions, }); }; // Handle when one or more dimensions are selected from the dropdown // Note that this is only available to metrics const handleAddDimensions = event => { const updatedDimensions = event.map( selectedDimension => selectedDimension.value, ); setSelection({ filters: selection.filters, dimensions: updatedDimensions, }); }; const filters = dimensions.map(grouping => { const dimensionsInGroup = grouping[1]; const dimensionGroupOptions = dimensionsInGroup .filter(dim => dim.properties.includes('primary_key') === true) .map(dim => { return { value: dim.name, label: labelize(dim.name.split('.').slice(-1)[0]), metadata: dim, }; }); return ( <> <div className="dimensionsList"> {dimensionGroupOptions.map(dimension => { return ( <DimensionFilter dimension={dimension} onChange={handleAddFilters} /> ); })} </div> </> ); }); return ( <Formik initialValues={initialValues} onSubmit={handleSubmit}> {function Render({ isSubmitting, status, setFieldValue }) { return ( <Form> <div className={'queryrunner'}> <div className="queryrunner-filters left"> {node?.type === 'metric' ? ( <> <span> <label>Group By</label> </span> <Select name="dimensions" options={dimensionsList} isMulti isClearable onChange={handleAddDimensions} /> <br /> </> ) : null} <span> <label>Add Filters</label> </span> {filters} </div> <div className={'righttop'}> <label>Generated Query</label> <div style={{ height: '200px', width: '80%', overflow: 'scroll', borderRadius: '0', border: '1px solid #ccc', }} className="queryrunner-query" > <SyntaxHighlighter language="sql" style={foundation} wrapLines={true} > {query} </SyntaxHighlighter> </div> <button type="submit" disabled={ running || (queryInfo !== null && queryInfo?.state !== 'FINISHED' && queryInfo?.state !== 'CANCELED' && queryInfo?.state !== 'FAILED') } className="button-3 execute-button" style={{ marginTop: '1rem' }} > {isSubmitting || running === true ? <LoadingIcon /> : '► Run'} </button> </div> <div style={{ width: window.innerWidth * 0.8, marginTop: '1rem', marginLeft: '0.5rem', height: 'calc(70% - 5.5px)', }} className={'rightbottom'} > <div className={'align-items-center row'} style={{ borderBottom: '1px solid #dddddd', width: '86%', }} > <div className={ 'tab-item' + (state.selectedTab === 'results' ? ' active' : '') } onClick={_ => switchTab('results')} > Results </div> <div className={ 'tab-item' + (state.selectedTab === 'info' ? ' active' : '') } aria-label={'QueryInfo'} role={'button'} onClick={_ => switchTab('info')} > Info </div> </div> {state.selectedTab === 'info' ? ( <div> {queryInfo && queryInfo.id ? ( <QueryInfo {...queryInfo} isList={true} /> ) : ( <></> )} </div> ) : null} {state.selectedTab === 'results' ? ( <div> {queryInfo !== null && queryInfo.state !== 'FINISHED' ? ( <div style={{ padding: '2rem' }}> The query has status {queryInfo.state}! Check the INFO tab for more details. </div> ) : queryInfo !== null && queryInfo.results !== null && queryInfo.results.length === 0 ? ( <div style={{ padding: '2rem' }}> The query finished but output no results. </div> ) : queryInfo !== null && queryInfo.results !== null && queryInfo.results.length > 0 ? ( <div className="table-responsive" style={{ gridGap: '0', width: '86%', padding: '0', maxHeight: '50%', }} > <table style={{ marginTop: '0 !important' }} className="table" > <thead className="fs-7 fw-bold text-gray-400 border-bottom-0"> <tr> {queryInfo.results[0]?.columns.map(columnName => ( <th key={columnName.name}> {columnName.column} </th> ))} </tr> </thead> <tbody> {queryInfo.results[0]?.rows .slice(0, 100) .map((rowData, index) => ( <tr key={`data-row:${index}`}> {rowData.map(rowValue => ( <td key={rowValue}>{rowValue}</td> ))} </tr> ))} </tbody> </table> </div> ) : ( <div style={{ padding: '2rem' }}> Click "Run" to execute the query. </div> )} </div> ) : null} </div> </div> </Form> ); }} </Formik> ); }