Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web console: misc fixes to the Explore view #17213

Merged
merged 16 commits into from
Oct 2, 2024
1 change: 1 addition & 0 deletions web-console/src/utils/local-storage-keys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const LocalStorageKeys = {
SQL_DATA_LOADER_CONTENT: 'sql-data-loader-content' as const,

EXPLORE_STATE: 'explore-state' as const,
EXPLORE_STICKY: 'explore-sticky' as const,
};
export type LocalStorageKeys = (typeof LocalStorageKeys)[keyof typeof LocalStorageKeys];

Expand Down
1 change: 1 addition & 0 deletions web-console/src/utils/table-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function changePage(pagination: Pagination, page: number): Pagination {
export interface ColumnHint {
displayName?: string;
group?: string;
hidden?: boolean;
expressionForWhere?: SqlExpression;
formatter?: (x: any) => string;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export const ControlPane = function ControlPane(props: ControlPaneProps) {
};
return {
element: (
<NamedExpressionsInput
<NamedExpressionsInput<ExpressionMeta>
allowReordering
values={effectiveValue ? [effectiveValue] : []}
onValuesChange={vs => onValueChange(vs[0])}
Expand Down Expand Up @@ -223,7 +223,7 @@ export const ControlPane = function ControlPane(props: ControlPaneProps) {
);
return {
element: (
<NamedExpressionsInput
<NamedExpressionsInput<ExpressionMeta>
allowReordering
values={effectiveValue as ExpressionMeta[]}
onValuesChange={onValueChange}
Expand Down Expand Up @@ -266,7 +266,7 @@ export const ControlPane = function ControlPane(props: ControlPaneProps) {
case 'measure': {
return {
element: (
<NamedExpressionsInput
<NamedExpressionsInput<Measure>
values={effectiveValue ? [effectiveValue] : []}
onValuesChange={vs => onValueChange(vs[0])}
singleton
Expand All @@ -284,9 +284,11 @@ export const ControlPane = function ControlPane(props: ControlPaneProps) {
/>
),
onDropColumn: column => {
const measures = Measure.getPossibleMeasuresForColumn(column);
if (!measures.length) return;
onValueChange(measures[0]);
const candidateMeasures = Measure.getPossibleMeasuresForColumn(column).filter(
p => !effectiveValue || effectiveValue.name !== p.name,
);
if (!candidateMeasures.length) return;
onValueChange(candidateMeasures[0]);
},
onDropMeasure: onValueChange,
};
Expand All @@ -313,11 +315,11 @@ export const ControlPane = function ControlPane(props: ControlPaneProps) {
/>
),
onDropColumn: column => {
const measures = Measure.getPossibleMeasuresForColumn(column).filter(
p => !effectiveValue.some((v: ExpressionMeta) => v.name === p.name),
const candidateMeasures = Measure.getPossibleMeasuresForColumn(column).filter(
p => !effectiveValue.some((v: Measure) => v.name === p.name),
);
if (!measures.length) return;
onValueChange(effectiveValue.concat(measures[0]));
if (!candidateMeasures.length) return;
onValueChange(effectiveValue.concat(candidateMeasures[0]));
},
onDropMeasure: measure => {
onValueChange(effectiveValue.concat(measure));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const NamedExpressionsInput = function NamedExpressionsInput<

const onDragOver = useCallback(
(e: React.DragEvent, i: number) => {
if (dragIndex === -1) return;
const targetRect = e.currentTarget.getBoundingClientRect();
const before = e.clientX - targetRect.left <= targetRect.width / 2;
setDropBefore(before);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const ContainsFilterControl = React.memo(function ContainsFilterControl(
),
)
.changeOrderByExpression(F.count().toOrderByExpression('DESC'))
.changeLimitValue(101)
.toString(),
// eslint-disable-next-line react-hooks/exhaustive-deps -- exclude 'makePattern' from deps
[querySource.query, filter, column, contains, negated],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const RegexpFilterControl = React.memo(function RegexpFilterControl(
SqlExpression.and(filter, regexp ? filterPatternToExpression(filterPattern) : undefined),
)
.changeOrderByExpression(F.count().toOrderByExpression('DESC'))
.changeLimitValue(101)
.toString(),
// eslint-disable-next-line react-hooks/exhaustive-deps -- exclude 'makePattern' from deps
[querySource.query, filter, column, regexp, negated],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
* limitations under the License.
*/

import { FormGroup, InputGroup, Menu, MenuItem } from '@blueprintjs/core';
import { FormGroup, Menu, MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { QueryResult, SqlQuery, ValuesFilterPattern } from '@druid-toolkit/query';
import { C, F, L, SqlExpression, SqlLiteral } from '@druid-toolkit/query';
import type { QueryResult, ValuesFilterPattern } from '@druid-toolkit/query';
import { C, F, SqlExpression, SqlQuery } from '@druid-toolkit/query';
import React, { useMemo, useState } from 'react';

import { ClearableInput } from '../../../../../../components';
import { useQueryManager } from '../../../../../../hooks';
import { caseInsensitiveContains } from '../../../../../../utils';
import { caseInsensitiveContains, filterMap } from '../../../../../../utils';
import type { QuerySource } from '../../../../models';
import { toggle } from '../../../../utils';
import { ColumnValue } from '../../column-value/column-value';
Expand All @@ -46,21 +47,21 @@ export const ValuesFilterControl = React.memo(function ValuesFilterControl(
const [initValues] = useState(selectedValues);
const [searchString, setSearchString] = useState('');

const valuesQuery = useMemo(() => {
const columnRef = C(column);
const queryParts: string[] = [`SELECT ${columnRef.as('c')}`, `FROM (${querySource.query})`];

const filterEx = SqlExpression.and(
filter,
searchString ? F('ICONTAINS_STRING', columnRef, L(searchString)) : undefined,
);
if (!(filterEx instanceof SqlLiteral)) {
queryParts.push(`WHERE ${filterEx}`);
}

queryParts.push(`GROUP BY 1 ORDER BY COUNT(*) DESC LIMIT 101`);
return queryParts.join('\n');
}, [querySource.query, filter, column, searchString]);
const valuesQuery = useMemo(
() =>
SqlQuery.from(querySource.query)
.addSelect(C(column).as('c'), { addToGroupBy: 'end' })
.changeWhereExpression(
SqlExpression.and(
filter,
searchString ? F('ICONTAINS_STRING', C(column), searchString) : undefined,
),
)
.changeOrderByExpression(F.count().toOrderByExpression('DESC'))
.changeLimitValue(101)
.toString(),
[querySource.query, filter, column, searchString],
);

const [valuesState] = useQueryManager<string, any[]>({
query: valuesQuery,
Expand All @@ -77,42 +78,37 @@ export const ValuesFilterControl = React.memo(function ValuesFilterControl(
if (values) {
valuesToShow = valuesToShow.concat(values.filter(v => !initValues.includes(v)));
}
if (searchString) {
valuesToShow = valuesToShow.filter(v => caseInsensitiveContains(v, searchString));
}

const showSearch = querySource.columns.find(c => c.name === column)?.sqlType !== 'BOOLEAN';

return (
<FormGroup className="values-filter-control">
{showSearch && (
<InputGroup
value={searchString}
onChange={e => setSearchString(e.target.value)}
placeholder="Search"
/>
<ClearableInput value={searchString} onChange={setSearchString} placeholder="Search" />
)}
<Menu className="value-list">
{valuesToShow.map((v, i) => (
<MenuItem
key={i}
icon={
selectedValues.includes(v)
? negated
? IconNames.DELETE
: IconNames.TICK_CIRCLE
: IconNames.CIRCLE
}
text={<ColumnValue value={v} />}
shouldDismissPopover={false}
onClick={e => {
setFilterPattern({
...filterPattern,
values: e.altKey ? [v] : toggle(selectedValues, v),
});
}}
/>
))}
{filterMap(valuesToShow, (v, i) => {
if (!caseInsensitiveContains(v, searchString)) return;
return (
<MenuItem
key={i}
icon={
selectedValues.includes(v)
? negated
? IconNames.DELETE
: IconNames.TICK_CIRCLE
: IconNames.CIRCLE
}
text={<ColumnValue value={v} />}
shouldDismissPopover={false}
onClick={e => {
setFilterPattern({
...filterPattern,
values: e.altKey ? [v] : toggle(selectedValues, v),
});
}}
/>
);
})}
{valuesState.loading && <MenuItem icon={IconNames.BLANK} text="Loading..." disabled />}
</Menu>
</FormGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ export const GenericOutputTable = React.memo(function GenericOutputTable(
columns={columnNester(
queryResult.header.map((column, i) => {
const h = column.name;
const hint = columnHints?.get(h);
const icon = showTypeIcons ? columnToIcon(column) : undefined;

return {
Expand All @@ -446,9 +447,10 @@ export const GenericOutputTable = React.memo(function GenericOutputTable(
},
headerClassName: getHeaderClassName(h),
accessor: String(i),
show: !hint?.hidden,
Cell(row) {
const value = row.value;
const formatter = columnHints?.get(h)?.formatter || formatNumber;
const formatter = hint?.formatter || formatNumber;
return (
<div>
<Popover content={<Deferred content={() => getCellMenu(column, i, value)} />}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,8 @@ export const ColumnDialog = React.memo(function ColumnDialog(props: ColumnDialog
if (!expression) return;
return SqlQuery.from(QuerySource.stripToBaseSource(querySource.query))
.addSelect(F.cast(expression, 'VARCHAR').as('v'), { addToGroupBy: 'end' })
.applyIf(
querySource.baseColumns.find(column => column.isTimeColumn()),
q => q.addWhere(sql`MAX_DATA_TIME() - INTERVAL '14' DAY <= __time`),
.applyIf(querySource.hasBaseTimeColumn(), q =>
q.addWhere(sql`MAX_DATA_TIME() - INTERVAL '14' DAY <= __time`),
)
.changeLimitValue(100)
.toString();
Expand Down Expand Up @@ -151,7 +150,7 @@ export const ColumnDialog = React.memo(function ColumnDialog(props: ColumnDialog
} else {
onApply(
querySource.changeColumn(initExpressionName, newExpression),
new Map([[initExpression.getOutputName()!, newExpression.getOutputName()!]]),
new Map([[initExpressionName, newExpression.getOutputName()!]]),
);
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,8 @@ export const MeasureDialog = React.memo(function MeasureDialog(props: MeasureDia
.changeWithParts([SqlWithPart.simple('t', QuerySource.stripToBaseSource(querySource.query))])
.addSelect(L('Overall').as('label'))
.addSelect(expression.as('value'))
.applyIf(
querySource.baseColumns.find(column => column.isTimeColumn()),
q => q.addWhere(sql`MAX_DATA_TIME() - INTERVAL '14' DAY <= __time`),
.applyIf(querySource.hasBaseTimeColumn(), q =>
q.addWhere(sql`MAX_DATA_TIME() - INTERVAL '14' DAY <= __time`),
)
.toString();
}, [querySource.query, formula]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

@import '../../../../../variables';

.nested-column-dialog {
&.#{$bp-ns}-dialog {
width: 50vw;
min-height: 540px;
}

.#{$bp-ns}-dialog-body {
display: flex;
flex-direction: column;

.path-selector {
flex: 1;
padding: 5px 0;
height: 400px;
overflow: auto;
border-left: 1px solid rgba(15, 19, 32, 0.4);
border-right: 1px solid rgba(15, 19, 32, 0.4);
}
}

.#{$bp-ns}-dialog-footer {
margin-top: 0;
}
}
Loading
Loading