blob: 0f4d771d5d18654773adf6cd91f7f34ec759cc78 [file] [log] [blame]
/*
* 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 {
Button,
ButtonGroup,
Intent,
Menu,
MenuItem,
Popover,
Position,
ResizeSensor,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { Timezone } from 'chronoshift';
import classNames from 'classnames';
import type { Column, QueryResult, SqlExpression, SqlQuery } from 'druid-query-toolkit';
import { SqlLiteral } from 'druid-query-toolkit';
import React, { useState } from 'react';
import { useMemoWithPrevious } from '../../../../hooks';
import {
isEmpty,
localStorageGetJson,
LocalStorageKeys,
localStorageSetJson,
mapRecord,
Stage,
} from '../../../../utils';
import type {
Measure,
ModuleState,
ParameterDefinition,
ParameterValues,
QuerySource,
} from '../../models';
import { effectiveParameterDefault, removeUndefinedParameterValues } from '../../models';
import { ModuleRepository } from '../../module-repository/module-repository';
import { adjustTransferValue, normalizeType } from '../../utils';
import { ControlPane } from '../control-pane/control-pane';
import { DroppableContainer } from '../droppable-container/droppable-container';
import { ErrorBoundary } from '../error-boundary/error-boundary';
import { FilterPane } from '../filter-pane/filter-pane';
import { Issue } from '../issue/issue';
import { ModulePicker } from '../module-picker/module-picker';
import './module-pane.scss';
function getStickyParameterValuesForModule(moduleId: string): ParameterValues {
return localStorageGetJson(LocalStorageKeys.EXPLORE_STICKY)?.[moduleId] || {};
}
function fillInDefaults(
parameterValues: ParameterValues,
previousParameterValues: ParameterValues | undefined,
parameters: Record<string, ParameterDefinition>,
querySource: QuerySource,
where: SqlExpression,
): Record<string, any> {
const parameterValuesWithDefaults = { ...parameterValues };
Object.entries(parameters).forEach(([propName, propDefinition]) => {
if (typeof parameterValuesWithDefaults[propName] !== 'undefined') return;
parameterValuesWithDefaults[propName] = effectiveParameterDefault(
propDefinition,
parameterValues,
querySource,
where,
previousParameterValues?.[propName],
);
});
return parameterValuesWithDefaults;
}
export interface ModulePaneProps {
className: string;
moduleState: ModuleState;
setModuleState(moduleState: ModuleState): void;
onDelete(): void;
querySource: QuerySource;
timezone: Timezone;
where: SqlExpression;
setWhere(where: SqlExpression): void;
runSqlQuery(
query: string | SqlQuery | { query: string | SqlQuery; timezone?: Timezone },
): Promise<QueryResult>;
onAddToSourceQueryAsColumn?(expression: SqlExpression): void;
onAddToSourceQueryAsMeasure?(measure: Measure): void;
}
export const ModulePane = function ModulePane(props: ModulePaneProps) {
const {
className,
moduleState,
setModuleState,
onDelete,
querySource,
timezone,
where,
setWhere,
runSqlQuery,
onAddToSourceQueryAsColumn,
onAddToSourceQueryAsMeasure,
} = props;
const { moduleId, moduleWhere, parameterValues, showModuleWhere, showControls } = moduleState;
const [stage, setStage] = useState<Stage | undefined>();
const module = ModuleRepository.getModule(moduleId);
function updateParameterValues(newParameterValues: ParameterValues) {
if (!module) return;
// Evaluate sticky-ness
const currentExploreSticky = localStorageGetJson(LocalStorageKeys.EXPLORE_STICKY) || {};
const currentModuleSticky = currentExploreSticky[moduleId] || {};
const newModuleSticky = {
...currentModuleSticky,
...mapRecord(newParameterValues, (v, k) => (module.parameters[k]?.sticky ? v : undefined)),
};
localStorageSetJson(LocalStorageKeys.EXPLORE_STICKY, {
...currentExploreSticky,
[moduleId]: isEmpty(newModuleSticky) ? undefined : newModuleSticky,
});
setModuleState(
moduleState.changeParameterValues(
removeUndefinedParameterValues(
{ ...parameterValues, ...newParameterValues },
module.parameters,
querySource,
where,
),
),
);
}
const parameterValuesWithDefaults: ParameterValues = useMemoWithPrevious(
previousParameterValuesWithDefaults => {
if (!module) return {};
return fillInDefaults(
parameterValues,
previousParameterValuesWithDefaults,
module.parameters,
querySource,
where,
);
},
[parameterValues, module, querySource],
);
let content: React.ReactNode;
if (module) {
const modelIssue = undefined; // AutoForm.issueWithModel(moduleTileConfig.config, module.configFields);
if (modelIssue) {
content = <Issue issue={modelIssue} />;
} else if (stage) {
content = React.createElement(module.component, {
stage,
querySource,
timezone,
where,
setWhere,
moduleWhere,
parameterValues: parameterValuesWithDefaults,
setParameterValues: updateParameterValues,
runSqlQuery,
});
}
} else {
content = <Issue issue={`Unknown module id: ${moduleId}`} />;
}
function onShowColumn(column: Column) {
setModuleState(moduleState.applyShowColumn(column));
}
function onShowMeasure(measure: Measure) {
setModuleState(moduleState.applyShowMeasure(measure));
}
const moduleHasFilter = !SqlLiteral.isTrue(moduleWhere);
return (
<div
className={classNames(
'module-pane',
className,
showControls ? 'show-controls' : 'no-controls',
showModuleWhere ? 'show-filter' : 'no-filter',
)}
>
{showModuleWhere && (
<FilterPane
querySource={querySource}
extraFilter={where}
timezone={timezone}
filter={moduleWhere}
onFilterChange={newFilter => setModuleState(moduleState.changeModuleWhere(newFilter))}
runSqlQuery={runSqlQuery}
/>
)}
<div className="module-control-bar">
<ModulePicker
selectedModuleId={moduleId}
onSelectedModuleIdChange={newModuleId => {
let newParameterValues = getStickyParameterValuesForModule(newModuleId);
const oldModule = ModuleRepository.getModule(moduleId);
const newModule = ModuleRepository.getModule(newModuleId);
if (oldModule && newModule) {
const oldModuleParameters = oldModule.parameters || {};
const newModuleParameters = newModule.parameters || {};
for (const paramName in oldModuleParameters) {
const parameterValue = parameterValues[paramName];
if (typeof parameterValue === 'undefined') continue;
const oldParameterDefinition = oldModuleParameters[paramName];
const transferGroup = oldParameterDefinition.transferGroup;
if (typeof transferGroup !== 'string') continue;
const normalizedType = normalizeType(oldParameterDefinition.type);
const target = Object.entries(newModuleParameters).find(
([_, def]) =>
def.transferGroup === transferGroup &&
normalizeType(def.type) === normalizedType,
);
if (!target) continue;
newParameterValues = {
...newParameterValues,
[target[0]]: adjustTransferValue(
parameterValue,
oldParameterDefinition.type,
target[1].type,
),
};
}
}
setModuleState(
moduleState.change({ moduleId: newModuleId, parameterValues: newParameterValues }),
);
}}
/>
{module && !showControls && (
<ControlPane
querySource={querySource}
where={where}
onUpdateParameterValues={updateParameterValues}
parameters={module.parameters}
parameterValues={parameterValuesWithDefaults}
compact
onAddToSourceQueryAsColumn={onAddToSourceQueryAsColumn}
onAddToSourceQueryAsMeasure={onAddToSourceQueryAsMeasure}
/>
)}
</div>
<ButtonGroup className="corner-buttons">
<Popover
position={Position.BOTTOM_RIGHT}
content={
<Menu>
<MenuItem
icon={IconNames.RESET}
text="Reset visualization parameters"
onClick={() => {
setModuleState(
moduleState.changeParameterValues(getStickyParameterValuesForModule(moduleId)),
);
}}
/>
<MenuItem
icon={IconNames.TRASH}
text="Delete module"
intent={Intent.DANGER}
onClick={onDelete}
/>
</Menu>
}
>
<Button icon={IconNames.MORE} data-tooltip="More module options" minimal />
</Popover>
<Button
icon={moduleHasFilter ? IconNames.FILTER_KEEP : IconNames.FILTER}
data-tooltip={`${showModuleWhere ? 'Hide' : 'Show'} module filter bar${
moduleHasFilter ? `\nCurrent filter: ${moduleWhere}` : ''
}`}
minimal
active={showModuleWhere}
onClick={() => setModuleState(moduleState.change({ showModuleWhere: !showModuleWhere }))}
/>
<Button
icon={IconNames.PROPERTIES}
data-tooltip={`${showControls ? 'Hide' : 'Show'} module controls`}
minimal
active={showControls}
onClick={() => setModuleState(moduleState.change({ showControls: !showControls }))}
/>
</ButtonGroup>
{showControls && module && (
<div className="control-pane-container">
<ControlPane
querySource={querySource}
where={where}
onUpdateParameterValues={updateParameterValues}
parameters={module.parameters}
parameterValues={parameterValuesWithDefaults}
onAddToSourceQueryAsColumn={onAddToSourceQueryAsColumn}
onAddToSourceQueryAsMeasure={onAddToSourceQueryAsMeasure}
/>
</div>
)}
<ResizeSensor
onResize={entries => {
if (entries.length !== 1) return;
const newStage = new Stage(entries[0].contentRect.width, entries[0].contentRect.height);
if (newStage.equals(stage)) return;
setStage(newStage);
}}
>
<DroppableContainer
className="module-inner-container"
onDropColumn={onShowColumn}
onDropMeasure={onShowMeasure}
>
<ErrorBoundary>{content}</ErrorBoundary>
</DroppableContainer>
</ResizeSensor>
</div>
);
};