| /* |
| * 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 { HotkeysProvider, Intent } from '@blueprintjs/core'; |
| import { IconNames } from '@blueprintjs/icons'; |
| import classNames from 'classnames'; |
| import type { JSX } from 'react'; |
| import React from 'react'; |
| import type { RouteComponentProps } from 'react-router'; |
| import { Redirect } from 'react-router'; |
| import { HashRouter, Route, Switch } from 'react-router-dom'; |
| import type { Filter } from 'react-table'; |
| |
| import { initAceDsqlMode } from './ace-modes/dsql'; |
| import { initAceHjsonMode } from './ace-modes/hjson'; |
| import { HeaderBar, Loader } from './components'; |
| import { SqlFunctionsProvider } from './contexts/sql-functions-context'; |
| import type { ConsoleViewId, QueryContext, QueryWithContext } from './druid-models'; |
| import type { AvailableFunctions } from './helpers'; |
| import { Capabilities, maybeGetClusterCapacity } from './helpers'; |
| import { stringToTableFilters, tableFiltersToString } from './react-table'; |
| import { AppToaster } from './singletons'; |
| import { compact, localStorageGetJson, LocalStorageKeys, QueryManager } from './utils'; |
| import { |
| DatasourcesView, |
| ExploreView, |
| HomeView, |
| LoadDataView, |
| LookupsView, |
| SegmentsView, |
| ServicesView, |
| SqlDataLoaderView, |
| SupervisorsView, |
| TasksView, |
| WorkbenchView, |
| } from './views'; |
| |
| import './console-application.scss'; |
| |
| type FiltersRouteMatch = RouteComponentProps<{ filters?: string }>; |
| |
| function changeTabWithFilter(tab: ConsoleViewId, filters: Filter[]) { |
| const filterString = tableFiltersToString(filters); |
| location.hash = tab + (filterString ? `/${filterString}` : ''); |
| } |
| |
| function viewFilterChange(tab: ConsoleViewId) { |
| return (filters: Filter[]) => changeTabWithFilter(tab, filters); |
| } |
| |
| function pathWithFilter(tab: ConsoleViewId) { |
| return `/${tab}/:filters?`; |
| } |
| |
| function switchTab(tab: ConsoleViewId) { |
| location.hash = tab; |
| } |
| |
| function switchToWorkbenchTab(tabId: string) { |
| location.hash = `workbench/${tabId}`; |
| } |
| |
| export interface ConsoleApplicationProps { |
| baseQueryContext?: QueryContext; |
| defaultQueryContext?: QueryContext; |
| mandatoryQueryContext?: QueryContext; |
| serverQueryContext?: QueryContext; |
| } |
| |
| export interface ConsoleApplicationState { |
| capabilities: Capabilities; |
| availableSqlFunctions?: AvailableFunctions; |
| capabilitiesLoading: boolean; |
| } |
| |
| export class ConsoleApplication extends React.PureComponent< |
| ConsoleApplicationProps, |
| ConsoleApplicationState |
| > { |
| private readonly capabilitiesQueryManager: QueryManager< |
| null, |
| [Capabilities, AvailableFunctions | undefined] |
| >; |
| |
| static shownServiceNotification() { |
| AppToaster.show({ |
| icon: IconNames.ERROR, |
| intent: Intent.DANGER, |
| timeout: 120000, |
| message: ( |
| <> |
| Some backend druid services are not responding. The console will not function at the |
| moment. Make sure that all your Druid services are up and running. Check the logs of |
| individual services for troubleshooting. |
| </> |
| ), |
| }); |
| } |
| |
| private supervisorId?: string; |
| private taskId?: string; |
| private openSupervisorDialog?: boolean; |
| private openTaskDialog?: boolean; |
| private queryWithContext?: QueryWithContext; |
| |
| constructor(props: ConsoleApplicationProps) { |
| super(props); |
| this.state = { |
| capabilities: Capabilities.FULL, |
| capabilitiesLoading: true, |
| }; |
| |
| this.capabilitiesQueryManager = new QueryManager({ |
| processQuery: async () => { |
| const capabilitiesOverride = localStorageGetJson(LocalStorageKeys.CAPABILITIES_OVERRIDE); |
| const capabilities = capabilitiesOverride |
| ? new Capabilities(capabilitiesOverride) |
| : await Capabilities.detectCapabilities(); |
| |
| if (!capabilities) { |
| ConsoleApplication.shownServiceNotification(); |
| return [Capabilities.FULL, undefined]; |
| } |
| |
| return Promise.all([ |
| Capabilities.detectCapacity(capabilities), |
| Capabilities.detectAvailableSqlFunctions(capabilities), |
| ]); |
| }, |
| onStateChange: ({ data, loading, error }) => { |
| if (error) { |
| console.error('There was an error retrieving the capabilities', error); |
| } |
| const capabilities = data?.[0] || Capabilities.FULL; |
| const availableSqlFunctions = data?.[1]; |
| initAceDsqlMode(availableSqlFunctions); |
| initAceHjsonMode(); |
| this.setState({ |
| capabilities, |
| availableSqlFunctions, |
| capabilitiesLoading: loading, |
| }); |
| }, |
| }); |
| } |
| |
| componentDidMount(): void { |
| this.capabilitiesQueryManager.runQuery(null); |
| } |
| |
| componentWillUnmount(): void { |
| this.capabilitiesQueryManager.terminate(); |
| } |
| |
| private readonly handleUnrestrict = (capabilities: Capabilities) => { |
| this.setState({ capabilities }); |
| }; |
| |
| private resetInitialsWithDelay() { |
| setTimeout(() => { |
| this.taskId = undefined; |
| this.supervisorId = undefined; |
| this.openSupervisorDialog = undefined; |
| this.openTaskDialog = undefined; |
| this.queryWithContext = undefined; |
| }, 50); |
| } |
| |
| private readonly goToStreamingDataLoader = (supervisorId?: string) => { |
| if (supervisorId) this.supervisorId = supervisorId; |
| switchTab('streaming-data-loader'); |
| this.resetInitialsWithDelay(); |
| }; |
| |
| private readonly goToClassicBatchDataLoader = (taskId?: string) => { |
| if (taskId) this.taskId = taskId; |
| switchTab('classic-batch-data-loader'); |
| this.resetInitialsWithDelay(); |
| }; |
| |
| private readonly goToDatasources = (datasource: string) => { |
| changeTabWithFilter('datasources', [{ id: 'datasource', value: `=${datasource}` }]); |
| }; |
| |
| private readonly goToSegments = ({ |
| start, |
| end, |
| datasource, |
| realtime, |
| }: { |
| start?: Date; |
| end?: Date; |
| datasource?: string; |
| realtime?: boolean; |
| }) => { |
| changeTabWithFilter( |
| 'segments', |
| compact([ |
| start && { id: 'start', value: `>=${start.toISOString()}` }, |
| end && { id: 'end', value: `<${end.toISOString()}` }, |
| datasource && { id: 'datasource', value: `=${datasource}` }, |
| typeof realtime === 'boolean' ? { id: 'is_realtime', value: `=${realtime}` } : undefined, |
| ]), |
| ); |
| }; |
| |
| private readonly goToSupervisor = (supervisorId: string) => { |
| changeTabWithFilter('supervisors', [{ id: 'supervisor_id', value: `=${supervisorId}` }]); |
| }; |
| |
| private readonly goToTasksWithTaskId = (taskId: string) => { |
| changeTabWithFilter('tasks', [{ id: 'task_id', value: `=${taskId}` }]); |
| }; |
| |
| private readonly goToTasksWithTaskGroupId = (taskGroupId: string) => { |
| changeTabWithFilter('tasks', [{ id: 'group_id', value: `=${taskGroupId}` }]); |
| }; |
| |
| private readonly goToTasksWithDatasource = (datasource: string, type?: string) => { |
| changeTabWithFilter( |
| 'tasks', |
| compact([ |
| { id: 'datasource', value: `=${datasource}` }, |
| type ? { id: 'type', value: `=${type}` } : undefined, |
| ]), |
| ); |
| }; |
| |
| private readonly openSupervisorSubmit = () => { |
| this.openSupervisorDialog = true; |
| switchTab('supervisors'); |
| this.resetInitialsWithDelay(); |
| }; |
| |
| private readonly openTaskSubmit = () => { |
| this.openTaskDialog = true; |
| switchTab('tasks'); |
| this.resetInitialsWithDelay(); |
| }; |
| |
| private readonly goToQuery = (queryWithContext: QueryWithContext) => { |
| this.queryWithContext = queryWithContext; |
| switchTab('workbench'); |
| this.resetInitialsWithDelay(); |
| }; |
| |
| private readonly wrapInViewContainer = ( |
| active: ConsoleViewId | null, |
| el: JSX.Element, |
| classType: 'normal' | 'narrow-pad' | 'thin' | 'thinner' = 'normal', |
| ) => { |
| const { capabilities } = this.state; |
| |
| return ( |
| <> |
| <HeaderBar |
| activeView={active} |
| capabilities={capabilities} |
| onUnrestrict={this.handleUnrestrict} |
| /> |
| <div className={classNames('view-container', classType)}>{el}</div> |
| </> |
| ); |
| }; |
| |
| private readonly wrappedHomeView = () => { |
| const { capabilities } = this.state; |
| return this.wrapInViewContainer(null, <HomeView capabilities={capabilities} />); |
| }; |
| |
| private readonly wrappedDataLoaderView = () => { |
| return this.wrapInViewContainer( |
| 'data-loader', |
| <LoadDataView |
| mode="all" |
| initTaskId={this.taskId} |
| initSupervisorId={this.supervisorId} |
| goToSupervisor={this.goToSupervisor} |
| goToTasks={this.goToTasksWithTaskGroupId} |
| openSupervisorSubmit={this.openSupervisorSubmit} |
| openTaskSubmit={this.openTaskSubmit} |
| />, |
| 'narrow-pad', |
| ); |
| }; |
| |
| private readonly wrappedStreamingDataLoaderView = () => { |
| return this.wrapInViewContainer( |
| 'streaming-data-loader', |
| <LoadDataView |
| mode="streaming" |
| initSupervisorId={this.supervisorId} |
| goToSupervisor={this.goToSupervisor} |
| goToTasks={this.goToTasksWithTaskGroupId} |
| openSupervisorSubmit={this.openSupervisorSubmit} |
| openTaskSubmit={this.openTaskSubmit} |
| />, |
| 'narrow-pad', |
| ); |
| }; |
| |
| private readonly wrappedClassicBatchDataLoaderView = () => { |
| return this.wrapInViewContainer( |
| 'classic-batch-data-loader', |
| <LoadDataView |
| mode="batch" |
| initTaskId={this.taskId} |
| goToSupervisor={this.goToSupervisor} |
| goToTasks={this.goToTasksWithTaskGroupId} |
| openSupervisorSubmit={this.openSupervisorSubmit} |
| openTaskSubmit={this.openTaskSubmit} |
| />, |
| 'narrow-pad', |
| ); |
| }; |
| |
| private readonly wrappedWorkbenchView = (p: RouteComponentProps<{ tabId?: string }>) => { |
| const { defaultQueryContext, mandatoryQueryContext, baseQueryContext, serverQueryContext } = |
| this.props; |
| const { capabilities } = this.state; |
| |
| return this.wrapInViewContainer( |
| 'workbench', |
| <WorkbenchView |
| capabilities={capabilities} |
| tabId={p.match.params.tabId} |
| onTabChange={switchToWorkbenchTab} |
| initQueryWithContext={this.queryWithContext} |
| defaultQueryContext={defaultQueryContext} |
| mandatoryQueryContext={mandatoryQueryContext} |
| baseQueryContext={baseQueryContext} |
| serverQueryContext={serverQueryContext} |
| queryEngines={capabilities.getSupportedQueryEngines()} |
| goToTask={this.goToTasksWithTaskId} |
| getClusterCapacity={maybeGetClusterCapacity} |
| />, |
| 'thin', |
| ); |
| }; |
| |
| private readonly wrappedSqlDataLoaderView = () => { |
| const { serverQueryContext } = this.props; |
| const { capabilities } = this.state; |
| return this.wrapInViewContainer( |
| 'sql-data-loader', |
| <SqlDataLoaderView |
| capabilities={capabilities} |
| goToQuery={this.goToQuery} |
| goToTask={this.goToTasksWithTaskId} |
| goToTaskGroup={this.goToTasksWithTaskGroupId} |
| getClusterCapacity={maybeGetClusterCapacity} |
| serverQueryContext={serverQueryContext} |
| />, |
| ); |
| }; |
| |
| private readonly wrappedDatasourcesView = (p: FiltersRouteMatch) => { |
| const { capabilities } = this.state; |
| return this.wrapInViewContainer( |
| 'datasources', |
| <DatasourcesView |
| filters={stringToTableFilters(p.match.params.filters)} |
| onFiltersChange={viewFilterChange('datasources')} |
| goToQuery={this.goToQuery} |
| goToTasks={this.goToTasksWithDatasource} |
| goToSegments={this.goToSegments} |
| capabilities={capabilities} |
| />, |
| ); |
| }; |
| |
| private readonly wrappedSegmentsView = (p: FiltersRouteMatch) => { |
| const { capabilities } = this.state; |
| return this.wrapInViewContainer( |
| 'segments', |
| <SegmentsView |
| filters={stringToTableFilters(p.match.params.filters)} |
| onFiltersChange={viewFilterChange('segments')} |
| goToQuery={this.goToQuery} |
| capabilities={capabilities} |
| />, |
| ); |
| }; |
| |
| private readonly wrappedSupervisorsView = (p: FiltersRouteMatch) => { |
| const { capabilities } = this.state; |
| return this.wrapInViewContainer( |
| 'supervisors', |
| <SupervisorsView |
| filters={stringToTableFilters(p.match.params.filters)} |
| onFiltersChange={viewFilterChange('supervisors')} |
| openSupervisorDialog={this.openSupervisorDialog} |
| goToDatasource={this.goToDatasources} |
| goToQuery={this.goToQuery} |
| goToStreamingDataLoader={this.goToStreamingDataLoader} |
| goToTasks={this.goToTasksWithTaskGroupId} |
| capabilities={capabilities} |
| />, |
| ); |
| }; |
| |
| private readonly wrappedTasksView = (p: FiltersRouteMatch) => { |
| const { capabilities } = this.state; |
| return this.wrapInViewContainer( |
| 'tasks', |
| <TasksView |
| filters={stringToTableFilters(p.match.params.filters)} |
| onFiltersChange={viewFilterChange('tasks')} |
| openTaskDialog={this.openTaskDialog} |
| goToDatasource={this.goToDatasources} |
| goToQuery={this.goToQuery} |
| goToClassicBatchDataLoader={this.goToClassicBatchDataLoader} |
| capabilities={capabilities} |
| />, |
| ); |
| }; |
| |
| private readonly wrappedServicesView = (p: FiltersRouteMatch) => { |
| const { capabilities } = this.state; |
| return this.wrapInViewContainer( |
| 'services', |
| <ServicesView |
| filters={stringToTableFilters(p.match.params.filters)} |
| onFiltersChange={viewFilterChange('services')} |
| goToQuery={this.goToQuery} |
| capabilities={capabilities} |
| />, |
| ); |
| }; |
| |
| private readonly wrappedLookupsView = (p: FiltersRouteMatch) => { |
| return this.wrapInViewContainer( |
| 'lookups', |
| <LookupsView |
| filters={stringToTableFilters(p.match.params.filters)} |
| onFiltersChange={viewFilterChange('lookups')} |
| />, |
| ); |
| }; |
| |
| render() { |
| const { capabilities, availableSqlFunctions, capabilitiesLoading } = this.state; |
| |
| if (capabilitiesLoading) { |
| return ( |
| <div className="loading-capabilities"> |
| <Loader /> |
| </div> |
| ); |
| } |
| |
| return ( |
| <HotkeysProvider> |
| <SqlFunctionsProvider availableSqlFunctions={availableSqlFunctions}> |
| <HashRouter hashType="noslash"> |
| <div className="console-application"> |
| <Switch> |
| {capabilities.hasCoordinatorAccess() && ( |
| <Route path="/data-loader" component={this.wrappedDataLoaderView} /> |
| )} |
| {capabilities.hasCoordinatorAccess() && ( |
| <Route |
| path="/streaming-data-loader" |
| component={this.wrappedStreamingDataLoaderView} |
| /> |
| )} |
| {capabilities.hasCoordinatorAccess() && ( |
| <Route |
| path="/classic-batch-data-loader" |
| component={this.wrappedClassicBatchDataLoaderView} |
| /> |
| )} |
| {capabilities.hasCoordinatorAccess() && capabilities.hasMultiStageQueryTask() && ( |
| <Route path="/sql-data-loader" component={this.wrappedSqlDataLoaderView} /> |
| )} |
| |
| <Route |
| path={pathWithFilter('supervisors')} |
| component={this.wrappedSupervisorsView} |
| /> |
| <Route path={pathWithFilter('tasks')} component={this.wrappedTasksView} /> |
| <Route path="/ingestion"> |
| <Redirect to="/tasks" /> |
| </Route> |
| |
| <Route |
| path={pathWithFilter('datasources')} |
| component={this.wrappedDatasourcesView} |
| /> |
| <Route path={pathWithFilter('segments')} component={this.wrappedSegmentsView} /> |
| <Route path={pathWithFilter('services')} component={this.wrappedServicesView} /> |
| |
| <Route path="/query"> |
| <Redirect to="/workbench" /> |
| </Route> |
| <Route |
| path={['/workbench/:tabId', '/workbench']} |
| component={this.wrappedWorkbenchView} |
| /> |
| |
| {capabilities.hasCoordinatorAccess() && ( |
| <Route path={pathWithFilter('lookups')} component={this.wrappedLookupsView} /> |
| )} |
| |
| {capabilities.hasSql() && ( |
| <Route |
| path="/explore" |
| component={() => <ExploreView capabilities={capabilities} />} |
| /> |
| )} |
| |
| <Route component={this.wrappedHomeView} /> |
| </Switch> |
| </div> |
| </HashRouter> |
| </SqlFunctionsProvider> |
| </HotkeysProvider> |
| ); |
| } |
| } |