datajunction-ui
Version:
DataJunction Metrics Platform UI
368 lines (348 loc) • 12.7 kB
JSX
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>
);
}