datajunction-ui
Version:
DataJunction UI
391 lines (380 loc) • 13.2 kB
JSX
import { useContext, useEffect, useState } from 'react';
import NamespaceHeader from '../../components/NamespaceHeader';
import { DataJunctionAPI } from '../../services/DJService';
import DJClientContext from '../../providers/djclient';
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
import foundation from 'react-syntax-highlighter/dist/esm/styles/hljs/foundation';
import Select from 'react-select';
import QueryInfo from '../../components/QueryInfo';
import 'react-querybuilder/dist/query-builder.scss';
import QueryBuilder, { formatQuery } from 'react-querybuilder';
import 'styles/styles.scss';
export function SQLBuilderPage() {
const DEFAULT_NUM_ROWS = 100;
const djClient = useContext(DJClientContext).DataJunctionAPI;
const validator = ruleType => !!ruleType.value;
const [stagedMetrics, setStagedMetrics] = useState([]);
const [metrics, setMetrics] = useState([]);
const [commonDimensionsList, setCommonDimensionsList] = useState([]);
const [selectedDimensions, setSelectedDimensions] = useState([]);
const [stagedDimensions, setStagedDimensions] = useState([]);
const [selectedMetrics, setSelectedMetrics] = useState([]);
const [query, setQuery] = useState('');
const [fields, setFields] = useState([]);
const [filters, setFilters] = useState({ combinator: 'and', rules: [] });
const [queryInfo, setQueryInfo] = useState({});
const [data, setData] = useState(null);
const [loadingData, setLoadingData] = useState(false);
const [viewData, setViewData] = useState(false);
const [showNumRows, setShowNumRows] = useState(DEFAULT_NUM_ROWS);
const [displayedRows, setDisplayedRows] = useState(<></>);
const numRowsOptions = [
{
value: 10,
label: '10 Rows',
isFixed: true,
},
{
value: 100,
label: '100 Rows',
isFixed: true,
},
{
value: 1000,
label: '1,000 Rows',
isFixed: true,
},
];
const toggleViewData = () => setViewData(current => !current);
// Get data for the current selection of metrics and dimensions
const getData = () => {
setLoadingData(true);
setQueryInfo({});
const fetchData = async () => {
if (process.env.REACT_USE_SSE) {
const sse = await djClient.stream(
selectedMetrics,
selectedDimensions,
formatQuery(filters, { format: 'sql', parseNumbers: true }),
);
sse.onmessage = e => {
const messageData = JSON.parse(JSON.parse(e.data));
setQueryInfo(messageData);
if (messageData.results) {
setLoadingData(false);
setData(messageData.results);
messageData.numRows = messageData.results?.length
? messageData.results[0].rows.length
: [];
setViewData(true);
setShowNumRows(DEFAULT_NUM_ROWS);
}
};
sse.onerror = () => sse.close();
} else {
const response = await djClient.data(
selectedMetrics,
selectedDimensions,
);
setQueryInfo(response);
if (response.results) {
setLoadingData(false);
setData(response.results);
response.numRows = response.results?.length
? response.results[0].rows.length
: [];
setViewData(true);
setShowNumRows(DEFAULT_NUM_ROWS);
}
}
};
fetchData().catch(console.error);
};
const resetView = () => {
setQuery('');
setData(null);
setViewData(false);
setQueryInfo({});
};
// Get metrics
useEffect(() => {
const fetchData = async () => {
const metrics = await djClient.metrics();
setMetrics(metrics.map(m => ({ value: m, label: m })));
};
fetchData().catch(console.error);
}, [djClient, djClient.metrics]);
const attributeToFormInput = dimension => {
const attribute = {
name: dimension.name,
label: `${dimension.name} (via ${dimension.path.join(' ▶ ')})`,
placeholder: `from ${dimension.path}`,
defaultOperator: '=',
validator,
};
if (dimension.type === 'bool') {
attribute.valueEditorType = 'checkbox';
}
if (dimension.type === 'timestamp') {
attribute.inputType = 'datetime-local';
attribute.defaultOperator = 'between';
}
return [dimension.name, attribute];
};
// Get common dimensions
useEffect(() => {
const fetchData = async () => {
if (selectedMetrics.length) {
const commonDimensions = await djClient.commonDimensions(
selectedMetrics,
);
setCommonDimensionsList(
commonDimensions.map(d => ({
value: d.name,
label: d.name,
path: d.path.join(' ▶ '),
})),
);
const uniqueFields = Object.fromEntries(
new Map(
commonDimensions.map(dimension => attributeToFormInput(dimension)),
),
);
setFields(Object.keys(uniqueFields).map(f => uniqueFields[f]));
} else {
setCommonDimensionsList([]);
setFields([]);
}
};
fetchData().catch(console.error);
}, [selectedMetrics, djClient]);
// Get SQL
useEffect(() => {
const fetchData = async () => {
if (
selectedMetrics.length > 0 &&
(selectedDimensions.length > 0 || filters.rules.length > 0)
) {
const result = await djClient.sqls(
selectedMetrics,
selectedDimensions,
formatQuery(filters, { format: 'sql', parseNumbers: true }),
);
setQuery(result.sql);
} else {
resetView();
}
};
fetchData().catch(console.error);
}, [djClient, filters, selectedDimensions, selectedMetrics]);
// Set number of rows to display
useEffect(() => {
if (data) {
setDisplayedRows(
data[0]?.rows.slice(0, showNumRows).map((rowData, index) => (
<tr key={`data-row:${index}`}>
{rowData.map((rowValue, colIndex) => (
<td key={`${index}-${colIndex}`}>{rowValue}</td>
))}
</tr>
)),
);
}
}, [showNumRows, data]);
const formatOptionLabel = ({ value, label, path }) => (
<div className={`badge dimension_option`}>
<div>{label}</div>
<span className={`badge dimension_option_subheading`}>{path}</span>
</div>
);
// @ts-ignore
return (
<>
<div className="mid">
<NamespaceHeader namespace="" />
<div className="card">
<div className="card-header">
<h4>Metrics</h4>
<span data-testid="select-metrics">
<Select
name="metrics"
options={metrics}
isDisabled={
!!(selectedMetrics.length && selectedDimensions.length)
}
noOptionsMessage={() => 'No metrics found.'}
placeholder={`${metrics.length} Available Metrics`}
isMulti
isClearable
closeMenuOnSelect={false}
onChange={e => {
setSelectedDimensions([]);
resetView();
setStagedMetrics(e.map(m => m.value));
setSelectedMetrics(stagedMetrics);
}}
onMenuClose={() => {
resetView();
setSelectedDimensions([]);
setSelectedMetrics(stagedMetrics);
}}
/>
</span>
<h4>Group By</h4>
<span data-testid="select-dimensions">
<Select
name="dimensions"
formatOptionLabel={formatOptionLabel}
options={commonDimensionsList}
noOptionsMessage={() =>
'No shared dimensions found. Try selecting different metrics.'
}
placeholder={`${commonDimensionsList.length} Shared Dimensions`}
isMulti
isClearable
closeMenuOnSelect={false}
onChange={e => {
resetView();
setStagedDimensions(e.map(d => d.value));
setSelectedDimensions(stagedDimensions);
}}
onMenuClose={() => {
setSelectedDimensions(stagedDimensions);
}}
/>
</span>
<h4>Filter By</h4>
<QueryBuilder
fields={fields}
query={filters}
onQueryChange={q => setFilters(q)}
/>
</div>
<div className="card-header">
{!viewData && !query ? (
<div className="card-light-shadow">
<h6>Using the SQL Builder</h6>
<p>
The sql builder allows you to group multiple metrics along
with their shared dimensions. Using your selections,
DataJunction will generate the corresponding SQL.
</p>
<ol>
<li>
<b>Select Metrics:</b> Start by selecting one or more
metrics from the metrics dropdown.
</li>
<li>
<b>Select Dimensions:</b> Next, select the dimension
attributes you would like to include. As you select
additional metrics, the list of available dimensions will be
filtered to those shared by the selected metrics. If the
dimensions list is empty, no shared dimensions were
discovered.
</li>
<li>
<b>View the generated SQL Query:</b> As you make your
selections, the SQL required to retrieve the set of metrics
and dimensions will be generated below.
</li>
<li>
<b>Run the Query:</b> If query running is enabled by your
server, you can also run the generated SQL query to view a
sample of 100 records.
</li>
</ol>
</div>
) : (
<></>
)}
{query ? (
<>
{loadingData ? (
<span
className="button-3 executing-button"
onClick={getData}
role="button"
aria-label="RunQuery"
aria-hidden="false"
>
{'Running Query'}
</span>
) : (
<span
className="button-3 execute-button"
onClick={getData}
role="button"
aria-label="RunQuery"
aria-hidden="false"
>
{'Run Query'}
</span>
)}
{data ? (
viewData ? (
<>
<span
className="button-3 neutral-button"
onClick={toggleViewData}
>
{'View Query'}
</span>
<span style={{ display: 'inline-block' }}>
<Select
name="num-rows"
defaultValue={numRowsOptions[0]}
options={numRowsOptions}
onChange={e => setShowNumRows(e.value)}
/>
</span>
</>
) : (
<span
className="button-3 neutral-button"
onClick={toggleViewData}
>
{'View Data'}
</span>
)
) : (
<></>
)}
</>
) : (
<></>
)}
{queryInfo && queryInfo.id ? <QueryInfo {...queryInfo} /> : <></>}
<div>
{query && !viewData ? (
<SyntaxHighlighter language="sql" style={foundation}>
{query}
</SyntaxHighlighter>
) : (
''
)}
</div>
{data && viewData ? (
<div className="table-responsive">
<table className="card-inner-table table">
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
<tr>
{data[0]?.columns.map(columnName => (
<th key={columnName.name}>{columnName.name}</th>
))}
</tr>
</thead>
<tbody>{displayedRows}</tbody>
</table>
</div>
) : (
<></>
)}
</div>
</div>
</div>
</>
);
}
SQLBuilderPage.defaultProps = {
djClient: DataJunctionAPI,
};