This is an automated email from the ASF dual-hosted git repository. vogievetsky pushed a commit to branch segment_timeline2 in repository https://gitbox.apache.org/repos/asf/druid.git
commit 289d95c7d09ab82c89f78ba5364a8b16ca855bf5 Author: Vadim Ogievetsky <[email protected]> AuthorDate: Fri Oct 25 15:03:53 2024 -0700 check in --- .../src/components/rule-editor/rule-editor.tsx | 4 +- .../src/components/segment-timeline/bar-group.tsx | 70 --- .../src/components/segment-timeline/common.ts | 34 +- ...ar-chart.scss => segment-bar-chart-render.scss} | 8 +- .../segment-timeline/segment-bar-chart-render.tsx | 155 +++++ .../segment-timeline/segment-bar-chart.scss} | 10 +- .../segment-timeline/segment-bar-chart.tsx | 194 ++++++ .../segment-timeline/segment-timeline.scss | 12 +- .../segment-timeline/segment-timeline.spec.tsx | 25 - .../segment-timeline/segment-timeline.tsx | 685 ++++----------------- .../segment-timeline/stacked-bar-chart.tsx | 148 ----- .../dialogs/retention-dialog/retention-dialog.tsx | 2 +- web-console/src/druid-models/index.ts | 1 + .../{utils => druid-models/load-rule}/load-rule.ts | 2 +- .../{views/explore-view/models => utils}/stage.ts | 14 + .../views/datasources-view/datasources-view.tsx | 5 +- .../components/module-pane/module-pane.tsx | 3 +- web-console/src/views/explore-view/models/index.ts | 1 - .../module-repository/module-repository.ts | 3 +- 19 files changed, 520 insertions(+), 856 deletions(-) diff --git a/web-console/src/components/rule-editor/rule-editor.tsx b/web-console/src/components/rule-editor/rule-editor.tsx index c5dfb24ddf0..0181e4341ba 100644 --- a/web-console/src/components/rule-editor/rule-editor.tsx +++ b/web-console/src/components/rule-editor/rule-editor.tsx @@ -30,9 +30,9 @@ import { import { IconNames } from '@blueprintjs/icons'; import React, { useState } from 'react'; +import type { Rule } from '../../druid-models'; +import { RuleUtil } from '../../druid-models'; import { durationSanitizer } from '../../utils'; -import type { Rule } from '../../utils/load-rule'; -import { RuleUtil } from '../../utils/load-rule'; import { SuggestibleInput } from '../suggestible-input/suggestible-input'; import './rule-editor.scss'; diff --git a/web-console/src/components/segment-timeline/bar-group.tsx b/web-console/src/components/segment-timeline/bar-group.tsx deleted file mode 100644 index 3c61859115a..00000000000 --- a/web-console/src/components/segment-timeline/bar-group.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 type { AxisScale } from 'd3-axis'; -import React from 'react'; - -import type { BarUnitData, HoveredBarInfo } from './common'; - -interface BarGroupProps { - dataToRender: BarUnitData[]; - changeActiveDatasource: (dataSource: string) => void; - xScale: AxisScale<Date>; - yScale: AxisScale<number>; - barWidth: number; - onHoverBar: (e: HoveredBarInfo) => void; -} - -export class BarGroup extends React.Component<BarGroupProps> { - render() { - const { dataToRender, changeActiveDatasource, xScale, yScale, onHoverBar, barWidth } = - this.props; - if (dataToRender === undefined) return null; - - return dataToRender.map((entry: BarUnitData, i: number) => { - const y0 = yScale(entry.y0 || 0) || 0; - const x = xScale(new Date(entry.x + 'T00:00:00Z')); - if (typeof x === 'undefined') return; - - const y = yScale((entry.y0 || 0) + entry.y) || 0; - const height = Math.max(y0 - y, 0); - const barInfo: HoveredBarInfo = { - xCoordinate: x, - yCoordinate: y, - height, - datasource: entry.datasource, - xValue: entry.x, - yValue: entry.y, - dailySize: entry.dailySize, - }; - return ( - <rect - key={i} - className="bar-unit" - x={x} - y={y} - width={barWidth} - height={height} - style={{ fill: entry.color }} - onClick={() => changeActiveDatasource(entry.datasource)} - onMouseOver={() => onHoverBar(barInfo)} - /> - ); - }); - } -} diff --git a/web-console/src/components/segment-timeline/common.ts b/web-console/src/components/segment-timeline/common.ts index 9add957e2c1..10a29751721 100644 --- a/web-console/src/components/segment-timeline/common.ts +++ b/web-console/src/components/segment-timeline/common.ts @@ -16,31 +16,19 @@ * limitations under the License. */ -export interface BarUnitData { - x: number; - y: number; - y0?: number; - width: number; - datasource: string; - color: string; - dailySize: number; -} +export type SegmentStat = 'count' | 'size' | 'rows'; -export interface Margin { - top: number; - right: number; - bottom: number; - left: number; +export interface SegmentRow extends Record<SegmentStat, number> { + start: string; + end: string; + datasource?: string; } -export interface HoveredBarInfo { - xCoordinate: number; - yCoordinate: number; - height: number; - datasource: string; - xValue: number; - yValue: number; - dailySize: number; +export interface SegmentBar extends SegmentRow { + startDate: Date; + endDate: Date; } -export type SegmentStat = 'sizeData' | 'countData'; +export interface StackedSegmentBar extends SegmentBar { + offset: Record<SegmentStat, number>; +} diff --git a/web-console/src/components/segment-timeline/stacked-bar-chart.scss b/web-console/src/components/segment-timeline/segment-bar-chart-render.scss similarity index 92% rename from web-console/src/components/segment-timeline/stacked-bar-chart.scss rename to web-console/src/components/segment-timeline/segment-bar-chart-render.scss index 26e5f5186b5..629499e13db 100644 --- a/web-console/src/components/segment-timeline/stacked-bar-chart.scss +++ b/web-console/src/components/segment-timeline/segment-bar-chart-render.scss @@ -16,13 +16,13 @@ * limitations under the License. */ -.stacked-bar-chart { +.segment-bar-chart-render { position: relative; overflow: hidden; .bar-chart-tooltip { position: absolute; - left: 100px; + left: 20px; right: 0; div { @@ -34,6 +34,10 @@ svg { position: absolute; + .chart-axis text { + user-select: none; + } + .hovered-bar { fill: transparent; stroke: #ffffff; diff --git a/web-console/src/components/segment-timeline/segment-bar-chart-render.tsx b/web-console/src/components/segment-timeline/segment-bar-chart-render.tsx new file mode 100644 index 00000000000..41823b50b0a --- /dev/null +++ b/web-console/src/components/segment-timeline/segment-bar-chart-render.tsx @@ -0,0 +1,155 @@ +/* + * 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 type { NonNullDateRange } from '@blueprintjs/datetime'; +import { max } from 'd3-array'; +import type { AxisScale } from 'd3-axis'; +import { axisBottom, axisLeft } from 'd3-axis'; +import { scaleLinear, scaleUtc } from 'd3-scale'; +import { useState } from 'react'; + +import { formatBytes, formatInteger } from '../../utils'; +import type { Margin, Stage } from '../../utils/stage'; + +import { ChartAxis } from './chart-axis'; +import type { SegmentBar, SegmentStat } from './common'; + +import './segment-bar-chart-render.scss'; + +const CHART_MARGIN: Margin = { top: 40, right: 5, bottom: 20, left: 60 }; + +interface SegmentBarChartRenderProps { + stage: Stage; + dateRange: NonNullDateRange; + shownSegmentStat: SegmentStat; + segmentBars: SegmentBar[]; + changeActiveDatasource(datasource: string | undefined): void; +} + +export const SegmentBarChartRender = function SegmentBarChartRender( + props: SegmentBarChartRenderProps, +) { + const { stage, shownSegmentStat, dateRange, segmentBars, changeActiveDatasource } = props; + const [hoverOn, setHoverOn] = useState<SegmentBar>(); + + const innerStage = stage.applyMargin(CHART_MARGIN); + + const timeScale: AxisScale<Date> = scaleUtc().domain(dateRange).range([0, innerStage.width]); + + const maxStat = max(segmentBars, d => d[shownSegmentStat]); + const statScale: AxisScale<number> = scaleLinear() + .rangeRound([innerStage.height, 0]) + .domain([0, maxStat ?? 1]); + + const formatTick = (n: number) => { + if (isNaN(n)) return ''; + if (shownSegmentStat === 'count') { + return formatInteger(n); + } else { + return formatBytes(n); + } + }; + + function segmentBarToRect(segmentBar: SegmentBar) { + const y0 = statScale(0)!; // segmentBar.y0 || + const xStart = timeScale(segmentBar.startDate)!; + const xEnd = timeScale(segmentBar.endDate)!; + const y = statScale(segmentBar[shownSegmentStat]) || 0; + + return { + x: xStart, + y: y, + width: Math.max(xEnd - xStart, 1), + height: Math.abs(y0 - y), + }; + } + + return ( + <div className="segment-bar-chart-render"> + {hoverOn && ( + <div className="bar-chart-tooltip"> + <div>Datasource: {hoverOn.datasource}</div> + <div>Time: {hoverOn.start}</div> + <div> + {`${shownSegmentStat === 'count' ? 'Count' : 'Size'}: ${formatTick( + hoverOn[shownSegmentStat], + )}`} + </div> + </div> + )} + <svg + width={stage.width} + height={stage.height} + viewBox={`0 0 ${stage.width} ${stage.height}`} + preserveAspectRatio="xMinYMin meet" + > + <g + transform={`translate(${CHART_MARGIN.left},${CHART_MARGIN.top})`} + onMouseLeave={() => setHoverOn(undefined)} + > + <ChartAxis + className="gridline-x" + transform="translate(0,0)" + axis={axisLeft(statScale) + .ticks(5) + .tickSize(-innerStage.width) + .tickFormat(() => '') + .tickSizeOuter(0)} + /> + <ChartAxis + className="axis-x" + transform={`translate(0,${innerStage.height})`} + axis={axisBottom(timeScale)} + /> + <ChartAxis + className="axis-y" + axis={axisLeft(statScale) + .ticks(5) + .tickFormat(e => formatTick(e))} + /> + {segmentBars.map((segmentBar, i) => { + return ( + <rect + key={i} + className="bar-unit" + {...segmentBarToRect(segmentBar)} + style={{ fill: i % 2 ? 'red' : 'blue' }} + onClick={ + segmentBar.datasource + ? () => changeActiveDatasource(segmentBar.datasource) + : undefined + } + onMouseOver={() => setHoverOn(segmentBar)} + /> + ); + })} + {hoverOn && ( + <rect + className="hovered-bar" + {...segmentBarToRect(hoverOn)} + onClick={() => { + setHoverOn(undefined); + changeActiveDatasource(hoverOn.datasource); + }} + /> + )} + </g> + </svg> + </div> + ); +}; diff --git a/web-console/src/views/explore-view/models/index.ts b/web-console/src/components/segment-timeline/segment-bar-chart.scss similarity index 79% copy from web-console/src/views/explore-view/models/index.ts copy to web-console/src/components/segment-timeline/segment-bar-chart.scss index 1e3c58ea09f..130997119e3 100644 --- a/web-console/src/views/explore-view/models/index.ts +++ b/web-console/src/components/segment-timeline/segment-bar-chart.scss @@ -16,10 +16,6 @@ * limitations under the License. */ -export * from './expression-meta'; -export * from './highlight'; -export * from './measure'; -export * from './measure-pattern'; -export * from './parameter'; -export * from './query-source'; -export * from './stage'; +.segment-bar-chart { + position: relative; +} diff --git a/web-console/src/components/segment-timeline/segment-bar-chart.tsx b/web-console/src/components/segment-timeline/segment-bar-chart.tsx new file mode 100644 index 00000000000..a6468ac3d4c --- /dev/null +++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx @@ -0,0 +1,194 @@ +/* + * 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 type { NonNullDateRange } from '@blueprintjs/datetime'; +import { C, F, N, sql, SqlQuery } from '@druid-toolkit/query'; +import { sum } from 'd3-array'; +import { useMemo } from 'react'; + +import type { Capabilities } from '../../helpers'; +import { useQueryManager } from '../../hooks'; +import { Api } from '../../singletons'; +import { filterMap, groupBy, queryDruidSql } from '../../utils'; +import type { Stage } from '../../utils/stage'; +import { Loader } from '../loader/loader'; + +import type { SegmentBar, SegmentRow, SegmentStat } from './common'; +import { SegmentBarChartRender } from './segment-bar-chart-render'; + +import './segment-bar-chart.scss'; + +type TrimDuration = 'PT1H' | 'P1D' | 'P1M' | 'P1Y'; + +function trimUtcDate(date: string, duration: TrimDuration): string { + // date like 2024-09-26T00:00:00.000Z + switch (duration) { + case 'PT1H': + return date.substring(0, 13) + ':00:00Z'; + + case 'P1D': + return date.substring(0, 10) + 'T00:00:00Z'; + + case 'P1M': + return date.substring(0, 7) + '-01T00:00:00Z'; + + case 'P1Y': + return date.substring(0, 4) + '-01-01T00:00:00Z'; + + default: + throw new Error(`Unexpected duration: ${duration}`); + } +} + +interface SegmentBarChartProps { + capabilities: Capabilities; + stage: Stage; + dateRange: NonNullDateRange; + breakByDataSource: boolean; + shownSegmentStat: SegmentStat; + changeActiveDatasource: (datasource: string | undefined) => void; +} + +export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartProps) { + const { + capabilities, + dateRange, + breakByDataSource, + stage, + shownSegmentStat, + changeActiveDatasource, + } = props; + + const intervalsQuery = useMemo( + () => ({ capabilities, dateRange, breakByDataSource }), + [capabilities, dateRange, breakByDataSource], + ); + + const [segmentRowsState] = useQueryManager({ + query: intervalsQuery, + processQuery: async ({ capabilities, dateRange, breakByDataSource }, cancelToken) => { + let segmentRows: SegmentRow[]; + if (capabilities.hasSql()) { + const query = SqlQuery.from(N('sys').table('segments')) + .changeWhereExpression( + sql`'${dateRange[0].toISOString()}' <= "start" AND "end" <= '${dateRange[1].toISOString()}' AND is_published = 1 AND is_overshadowed = 0`, + ) + .addSelect(C('start'), { addToGroupBy: 'end' }) + .addSelect(C('end'), { addToGroupBy: 'end' }) + .applyIf(breakByDataSource, q => q.addSelect(C('datasource'), { addToGroupBy: 'end' })) + .addSelect(F.count().as('count')) + .addSelect(F.sum(C('size')).as('size')) + .addSelect(F.sum(C('num_rows')).as('rows')); + + segmentRows = await queryDruidSql({ query: query.toString() }); + } else { + const datasources: string[] = ( + await Api.instance.get(`/druid/coordinator/v1/datasources`, { cancelToken }) + ).data; + segmentRows = ( + await Promise.all( + datasources.map(async datasource => { + const intervalMap = ( + await Api.instance.get( + `/druid/coordinator/v1/datasources/${Api.encodePath( + datasource, + )}/intervals?simple`, + { cancelToken }, + ) + ).data; + + return filterMap(Object.entries(intervalMap), ([interval, v]) => { + // ToDo: Filter on start end + const [start, end] = interval.split('/'); + const { count, size, rows } = v as any; + return { + start, + end, + datasource: breakByDataSource ? datasource : undefined, + count, + size, + rows, + }; + }); + }), + ) + ).flat(); + } + + const trimDuration: TrimDuration = 'P1D'; + return groupBy( + segmentRows, + segmentRow => + // This trimming should ideally be pushed into the SQL query but at the time of this writing queries on the sys.* tables do not allow substring + `${trimUtcDate(segmentRow.start, trimDuration)}/${trimUtcDate( + segmentRow.end, + trimDuration, + )}/${segmentRow.datasource || ''}`, + (segmentRows): SegmentBar => { + const firstRow = segmentRows[0]; + const start = trimUtcDate(firstRow.start, trimDuration); + const end = trimUtcDate(firstRow.end, trimDuration); + return { + ...firstRow, + start, + startDate: new Date(start), + end, + endDate: new Date(end), + count: sum(segmentRows, s => s.count), + size: sum(segmentRows, s => s.size), + rows: sum(segmentRows, s => s.rows), + }; + }, + ); + }, + }); + + if (segmentRowsState.loading) { + return <Loader />; + } + + if (segmentRowsState.error) { + return ( + <div className="empty-placeholder"> + <span className="no-data-text">{`Error when loading data: ${segmentRowsState.getErrorMessage()}`}</span> + </div> + ); + } + + const segmentRows = segmentRowsState.data; + if (!segmentRows) return null; + + if (!segmentRows.length) { + return ( + <div className="empty-placeholder"> + <span className="no-data-text">There are no segments for the selected interval</span> + </div> + ); + } + + console.log(segmentRows); + return ( + <SegmentBarChartRender + stage={stage} + dateRange={dateRange} + shownSegmentStat={shownSegmentStat} + segmentBars={segmentRows} + changeActiveDatasource={changeActiveDatasource} + /> + ); +}; diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss b/web-console/src/components/segment-timeline/segment-timeline.scss index 6239e717ae8..583ce290ac9 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.scss +++ b/web-console/src/components/segment-timeline/segment-timeline.scss @@ -32,7 +32,17 @@ padding: 10px; } - .stacked-bar-chart { + .chart-container { + position: absolute; + width: 100%; + height: 100%; + overflow: hidden; + } + + .segment-bar-chart, + .segment-bar-chart-render { + position: absolute; + width: 100%; height: 100%; } diff --git a/web-console/src/components/segment-timeline/segment-timeline.spec.tsx b/web-console/src/components/segment-timeline/segment-timeline.spec.tsx index 4f95842801a..f247f98794a 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.spec.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.spec.tsx @@ -16,38 +16,13 @@ * limitations under the License. */ -import { sane } from '@druid-toolkit/query'; import { render } from '@testing-library/react'; import { Capabilities } from '../../helpers'; import { SegmentTimeline } from './segment-timeline'; -jest.useFakeTimers('modern').setSystemTime(Date.parse('2021-06-08T12:34:56Z')); - describe('SegmentTimeline', () => { - it('.getSqlQuery', () => { - expect( - SegmentTimeline.getSqlQuery([ - new Date('2020-01-01T00:00:00Z'), - new Date('2021-02-01T00:00:00Z'), - ]), - ).toEqual(sane` - SELECT - "start", "end", "datasource", - COUNT(*) AS "count", - SUM("size") AS "size" - FROM sys.segments - WHERE - '2020-01-01T00:00:00.000Z' <= "start" AND - "end" <= '2021-02-01T00:00:00.000Z' AND - is_published = 1 AND - is_overshadowed = 0 - GROUP BY 1, 2, 3 - ORDER BY "start" DESC - `); - }); - it('matches snapshot', () => { const segmentTimeline = <SegmentTimeline capabilities={Capabilities.FULL} />; const { container } = render(segmentTimeline); diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx index edb7dfcf60f..df4e1c12960 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.tsx @@ -16,40 +16,27 @@ * limitations under the License. */ -import { - Button, - FormGroup, - MenuItem, - ResizeSensor, - SegmentedControl, - Tag, -} from '@blueprintjs/core'; -import type { DateRange, NonNullDateRange } from '@blueprintjs/datetime'; +import { Button, FormGroup, MenuItem, ResizeSensor, SegmentedControl } from '@blueprintjs/core'; +import type { NonNullDateRange } from '@blueprintjs/datetime'; import { DateRangeInput3 } from '@blueprintjs/datetime2'; import { IconNames } from '@blueprintjs/icons'; -import type { ItemPredicate, ItemRenderer } from '@blueprintjs/select'; import { Select } from '@blueprintjs/select'; -import type { AxisScale } from 'd3-axis'; -import { scaleLinear, scaleUtc } from 'd3-scale'; import enUS from 'date-fns/locale/en-US'; -import React from 'react'; +import type React from 'react'; +import { useState } from 'react'; import type { Capabilities } from '../../helpers'; -import { Api } from '../../singletons'; import { ceilToUtcDay, isNonNullRange, localToUtcDateRange, - queryDruidSql, - QueryManager, - uniq, utcToLocalDateRange, } from '../../utils'; -import { Loader } from '../loader/loader'; +import { Stage } from '../../utils/stage'; import { SplitterLayout } from '../splitter-layout/splitter-layout'; -import type { BarUnitData, SegmentStat } from './common'; -import { StackedBarChart } from './stacked-bar-chart'; +import type { SegmentStat } from './common'; +import { SegmentBarChart } from './segment-bar-chart'; import './segment-timeline.scss'; @@ -57,37 +44,6 @@ interface SegmentTimelineProps { capabilities: Capabilities; } -interface SegmentTimelineState { - chartHeight: number; - chartWidth: number; - data?: Record<string, any>; - datasources: string[]; - stackedData?: Record<string, BarUnitData[]>; - singleDatasourceData?: Record<string, Record<string, BarUnitData[]>>; - activeDatasource?: string; - activeSegmentStat: SegmentStat; - dataToRender: BarUnitData[]; - loading: boolean; - error?: Error; - xScale: AxisScale<Date> | null; - yScale: AxisScale<number> | null; - dateRange: NonNullDateRange; - selectedDateRange?: DateRange; -} - -interface BarChartScales { - xScale: AxisScale<Date>; - yScale: AxisScale<number>; -} - -interface IntervalRow { - start: string; - end: string; - datasource: string; - count: number; - size: number; -} - const DEFAULT_TIME_SPAN_MONTHS = 3; function getDefaultDateRange(): NonNullDateRange { @@ -97,533 +53,122 @@ function getDefaultDateRange(): NonNullDateRange { return [start, end]; } -export class SegmentTimeline extends React.PureComponent< - SegmentTimelineProps, - SegmentTimelineState -> { - static COLORS = [ - '#b33040', - '#d25c4d', - '#f2b447', - '#d9d574', - '#4FAA7E', - '#57ceff', - '#789113', - '#098777', - '#b33040', - '#d2757b', - '#f29063', - '#d9a241', - '#80aa61', - '#c4ff9e', - '#915412', - '#87606c', - ]; - - static getColor(index: number): string { - return SegmentTimeline.COLORS[index % SegmentTimeline.COLORS.length]; - } - - static getSqlQuery(dateRange: NonNullDateRange): string { - return `SELECT - "start", "end", "datasource", - COUNT(*) AS "count", - SUM("size") AS "size" -FROM sys.segments -WHERE - '${dateRange[0].toISOString()}' <= "start" AND - "end" <= '${dateRange[1].toISOString()}' AND - is_published = 1 AND - is_overshadowed = 0 -GROUP BY 1, 2, 3 -ORDER BY "start" DESC`; - } - - static processRawData(data: IntervalRow[]) { - if (data === null) return []; - - const countData: Record<string, any> = {}; - const sizeData: Record<string, any> = {}; - data.forEach(entry => { - const start = entry.start; - const day = start.split('T')[0]; - const datasource = entry.datasource; - const count = entry.count; - const segmentSize = entry.size; - if (countData[day] === undefined) { - countData[day] = { - day, - [datasource]: count, - total: count, - }; - sizeData[day] = { - day, - [datasource]: segmentSize, - total: segmentSize, - }; - } else { - const countDataEntry: number | undefined = countData[day][datasource]; - countData[day][datasource] = count + (countDataEntry === undefined ? 0 : countDataEntry); - const sizeDataEntry: number | undefined = sizeData[day][datasource]; - sizeData[day][datasource] = segmentSize + (sizeDataEntry === undefined ? 0 : sizeDataEntry); - countData[day].total += count; - sizeData[day].total += segmentSize; - } - }); - - const countDataArray = Object.keys(countData) - .reverse() - .map((time: any) => { - return countData[time]; - }); - - const sizeDataArray = Object.keys(sizeData) - .reverse() - .map((time: any) => { - return sizeData[time]; - }); - - return { countData: countDataArray, sizeData: sizeDataArray }; - } - - static calculateStackedData( - data: Record<string, any>, - datasources: string[], - ): Record<string, BarUnitData[]> { - const newStackedData: Record<string, BarUnitData[]> = {}; - Object.keys(data).forEach((type: any) => { - const stackedData: any = data[type].map((d: any) => { - let y0 = 0; - return datasources.map((datasource: string, i) => { - const barUnitData = { - x: d.day, - y: d[datasource] === undefined ? 0 : d[datasource], - y0, - datasource, - color: SegmentTimeline.getColor(i), - dailySize: d.total, - }; - y0 += d[datasource] === undefined ? 0 : d[datasource]; - return barUnitData; - }); - }); - newStackedData[type] = stackedData.flat(); - }); - - return newStackedData; - } - - static calculateSingleDatasourceData( - data: Record<string, any>, - datasources: string[], - ): Record<string, Record<string, BarUnitData[]>> { - const singleDatasourceData: Record<string, Record<string, BarUnitData[]>> = {}; - Object.keys(data).forEach(dataType => { - singleDatasourceData[dataType] = {}; - datasources.forEach((datasource, i) => { - const currentData = data[dataType]; - if (currentData.length === 0) return; - const dataResult = currentData.map((d: any) => { - let y = 0; - if (d[datasource] !== undefined) { - y = d[datasource]; - } - return { - x: d.day, - y, - datasource, - color: SegmentTimeline.getColor(i), - dailySize: d.total, - }; - }); - if (!dataResult.every((d: any) => d.y === 0)) { - singleDatasourceData[dataType][datasource] = dataResult; - } - }); - }); - - return singleDatasourceData; - } - - private readonly dataQueryManager: QueryManager< - { capabilities: Capabilities; dateRange: NonNullDateRange }, - any - >; - - private readonly chartMargin = { top: 40, right: 0, bottom: 20, left: 60 }; - - constructor(props: SegmentTimelineProps) { - super(props); - const dateRange = getDefaultDateRange(); - - this.state = { - chartWidth: 1, // Dummy init values to be replaced - chartHeight: 1, // after first render - data: {}, - datasources: [], - stackedData: {}, - singleDatasourceData: {}, - dataToRender: [], - activeSegmentStat: 'sizeData', - loading: true, - xScale: null, - yScale: null, - dateRange, - }; - - this.dataQueryManager = new QueryManager({ - processQuery: async ({ capabilities, dateRange }, cancelToken) => { - let intervals: IntervalRow[]; - let datasources: string[]; - if (capabilities.hasSql()) { - intervals = await queryDruidSql( - { - query: SegmentTimeline.getSqlQuery(dateRange), - }, - cancelToken, - ); - datasources = uniq(intervals.map(r => r.datasource).sort()); - } else if (capabilities.hasCoordinatorAccess()) { - const startIso = dateRange[0].toISOString(); - - datasources = ( - await Api.instance.get(`/druid/coordinator/v1/datasources`, { cancelToken }) - ).data; - intervals = ( - await Promise.all( - datasources.map(async datasource => { - const intervalMap = ( - await Api.instance.get( - `/druid/coordinator/v1/datasources/${Api.encodePath( - datasource, - )}/intervals?simple`, - { cancelToken }, - ) - ).data; - - return Object.keys(intervalMap) - .map(interval => { - const [start, end] = interval.split('/'); - const { count, size } = intervalMap[interval]; - return { - start, - end, - datasource, - count, - size, - }; - }) - .filter(a => startIso < a.start); - }), - ) - ) - .flat() - .sort((a, b) => b.start.localeCompare(a.start)); - } else { - throw new Error(`must have SQL or coordinator access`); - } - - const data = SegmentTimeline.processRawData(intervals); - const stackedData = SegmentTimeline.calculateStackedData(data, datasources); - const singleDatasourceData = SegmentTimeline.calculateSingleDatasourceData( - data, - datasources, - ); - return { data, datasources, stackedData, singleDatasourceData }; - }, - onStateChange: ({ data, loading, error }) => { - this.setState({ - data: data ? data.data : undefined, - datasources: data ? data.datasources : [], - stackedData: data ? data.stackedData : undefined, - singleDatasourceData: data ? data.singleDatasourceData : undefined, - loading, - error, - }); - }, - }); - } - - componentDidMount(): void { - const { capabilities } = this.props; - const { dateRange } = this.state; - - if (isNonNullRange(dateRange)) { - this.dataQueryManager.runQuery({ capabilities, dateRange }); - } - } - - componentWillUnmount(): void { - this.dataQueryManager.terminate(); - } - - componentDidUpdate(_prevProps: SegmentTimelineProps, prevState: SegmentTimelineState): void { - const { activeDatasource, activeSegmentStat, singleDatasourceData, stackedData } = this.state; - if ( - prevState.data !== this.state.data || - prevState.activeSegmentStat !== this.state.activeSegmentStat || - prevState.activeDatasource !== this.state.activeDatasource || - prevState.chartWidth !== this.state.chartWidth || - prevState.chartHeight !== this.state.chartHeight - ) { - const scales: BarChartScales | undefined = this.calculateScales(); - const dataToRender: BarUnitData[] | undefined = activeDatasource - ? singleDatasourceData - ? singleDatasourceData[activeSegmentStat][activeDatasource] - : undefined - : stackedData - ? stackedData[activeSegmentStat] - : undefined; - - if (scales && dataToRender) { - this.setState({ - dataToRender, - xScale: scales.xScale, - yScale: scales.yScale, - }); - } - } - } - - private calculateScales(): BarChartScales | undefined { - const { - chartWidth, - chartHeight, - data, - activeSegmentStat, - activeDatasource, - singleDatasourceData, - dateRange, - } = this.state; - if (!data || !Object.keys(data).length || !isNonNullRange(dateRange)) return; - const activeData = data[activeSegmentStat]; - - let yMax = - activeData.length === 0 - ? 100 - : activeData.reduce((max: any, d: any) => (max.total > d.total ? max : d)).total; - - if ( - activeDatasource && - singleDatasourceData![activeSegmentStat][activeDatasource] !== undefined - ) { - yMax = singleDatasourceData![activeSegmentStat][activeDatasource].reduce((max: any, d: any) => - max.y > d.y ? max : d, - ).y; - } - - const xScale: AxisScale<Date> = scaleUtc() - .domain(dateRange) - .range([0, chartWidth - this.chartMargin.left - this.chartMargin.right]); - - const yScale: AxisScale<number> = scaleLinear() - .rangeRound([chartHeight - this.chartMargin.top - this.chartMargin.bottom, 0]) - .domain([0, yMax]); +export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelineProps) { + const { capabilities } = props; + const [stage, setStage] = useState<Stage | undefined>(); + const [activeSegmentStat, setActiveSegmentStat] = useState<SegmentStat>('size'); + const [activeDatasource, setActiveDatasource] = useState<string | undefined>(); + const [dateRange, setDateRange] = useState<NonNullDateRange>(getDefaultDateRange); - return { - xScale, - yScale, - }; - } + const datasources: string[] = ['wiki', 'kttm']; - private readonly handleResize = (entries: ResizeObserverEntry[]) => { - const chartRect = entries[0].contentRect; - this.setState({ - chartWidth: chartRect.width, - chartHeight: chartRect.height, - }); - }; - - renderStackedBarChart() { - const { - chartWidth, - chartHeight, - loading, - dataToRender, - activeSegmentStat, - error, - xScale, - yScale, - data, - activeDatasource, - dateRange, - } = this.state; - - if (loading) { - return <Loader />; - } - - if (error) { - return ( - <div className="empty-placeholder"> - <span className="no-data-text">Error when loading data: {error.message}</span> - </div> - ); - } - - if (xScale === null || yScale === null) { - return ( - <div className="empty-placeholder"> - <span className="no-data-text">Error when calculating scales</span> - </div> - ); - } - - if (data![activeSegmentStat].length === 0) { - return ( - <div className="empty-placeholder"> - <span className="no-data-text">There are no segments for the selected interval</span> - </div> - ); - } - - if ( - activeDatasource && - data![activeSegmentStat].every((d: any) => d[activeDatasource] === undefined) - ) { - return ( - <div className="empty-placeholder"> - <span className="no-data-text"> - No data available for <Tag minimal>{activeDatasource}</Tag> - </span> - </div> - ); - } - - const millisecondsPerDay = 24 * 60 * 60 * 1000; - const barCounts = (dateRange[1].getTime() - dateRange[0].getTime()) / millisecondsPerDay; - const barWidth = Math.max( - 0, - (chartWidth - this.chartMargin.left - this.chartMargin.right) / barCounts, - ); + const DatasourceSelect: React.FC = () => { + const showAll = 'Show all'; + const datasourcesWzAll = [showAll].concat(datasources); return ( - <ResizeSensor onResize={this.handleResize}> - <StackedBarChart - dataToRender={dataToRender} - svgHeight={chartHeight} - svgWidth={chartWidth} - margin={this.chartMargin} - changeActiveDatasource={(datasource: string | undefined) => - this.setState(prevState => ({ - activeDatasource: prevState.activeDatasource ? undefined : datasource, - })) + <Select<string> + items={datasourcesWzAll} + onItemSelect={(selectedItem: string) => { + setActiveDatasource(selectedItem === showAll ? undefined : selectedItem); + }} + itemRenderer={(val, { handleClick, handleFocus, modifiers }) => { + if (!modifiers.matchesPredicate) return null; + return ( + <MenuItem + key={val} + disabled={modifiers.disabled} + active={modifiers.active} + onClick={handleClick} + onFocus={handleFocus} + roleStructure="listoption" + text={val} + /> + ); + }} + noResults={<MenuItem disabled text="No results" roleStructure="listoption" />} + itemPredicate={(query, val, _index, exactMatch) => { + const normalizedTitle = val.toLowerCase(); + const normalizedQuery = query.toLowerCase(); + + if (exactMatch) { + return normalizedTitle === normalizedQuery; + } else { + return normalizedTitle.includes(normalizedQuery); } - shownSegmentStat={activeSegmentStat} - xScale={xScale} - yScale={yScale} - barWidth={barWidth} + }} + fill + > + <Button + text={activeDatasource === null ? showAll : activeDatasource} + fill + rightIcon={IconNames.CARET_DOWN} /> - </ResizeSensor> + </Select> ); - } - - render() { - const { capabilities } = this.props; - const { datasources, activeSegmentStat, activeDatasource, dateRange, selectedDateRange } = - this.state; - - const filterDatasource: ItemPredicate<string> = (query, val, _index, exactMatch) => { - const normalizedTitle = val.toLowerCase(); - const normalizedQuery = query.toLowerCase(); - - if (exactMatch) { - return normalizedTitle === normalizedQuery; - } else { - return normalizedTitle.includes(normalizedQuery); - } - }; - - const datasourceRenderer: ItemRenderer<string> = ( - val, - { handleClick, handleFocus, modifiers }, - ) => { - if (!modifiers.matchesPredicate) return null; - return ( - <MenuItem - key={val} - disabled={modifiers.disabled} - active={modifiers.active} - onClick={handleClick} - onFocus={handleFocus} - roleStructure="listoption" - text={val} - /> - ); - }; - - const DatasourceSelect: React.FC = () => { - const showAll = 'Show all'; - const handleItemSelected = (selectedItem: string) => { - this.setState({ - activeDatasource: selectedItem === showAll ? undefined : selectedItem, - }); - }; - const datasourcesWzAll = [showAll].concat(datasources); - return ( - <Select<string> - items={datasourcesWzAll} - onItemSelect={handleItemSelected} - itemRenderer={datasourceRenderer} - noResults={<MenuItem disabled text="No results" roleStructure="listoption" />} - itemPredicate={filterDatasource} - fill - > - <Button - text={activeDatasource === null ? showAll : activeDatasource} - fill - rightIcon={IconNames.CARET_DOWN} - /> - </Select> - ); - }; + }; - return ( - <SplitterLayout - className="segment-timeline" - primaryMinSize={400} - secondaryInitialSize={220} - secondaryMaxSize={400} + return ( + <SplitterLayout + className="segment-timeline" + primaryMinSize={400} + secondaryInitialSize={220} + secondaryMaxSize={400} + > + <ResizeSensor + onResize={(entries: ResizeObserverEntry[]) => { + const rect = entries[0].contentRect; + setStage(new Stage(rect.width, rect.height)); + }} > - {this.renderStackedBarChart()} - <div className="side-control"> - <FormGroup label="Show"> - <SegmentedControl - value={activeSegmentStat} - onValueChange={activeDataType => - this.setState({ activeSegmentStat: activeDataType as SegmentStat }) + <div className="chart-container"> + {stage && ( + <SegmentBarChart + capabilities={capabilities} + stage={stage} + dateRange={dateRange} + shownSegmentStat={activeSegmentStat} + breakByDataSource={false} + changeActiveDatasource={(datasource: string | undefined) => + setActiveDatasource(activeDatasource ? undefined : datasource) } - options={[ - { - label: 'Total size', - value: 'sizeData', - }, - { - label: 'Segment count', - value: 'countData', - }, - ]} - fill - /> - </FormGroup> - <FormGroup label="Interval"> - <DateRangeInput3 - value={utcToLocalDateRange(selectedDateRange || dateRange)} - onChange={newDateRange => { - const newUtcDateRange = localToUtcDateRange(newDateRange); - if (!isNonNullRange(newUtcDateRange)) return; - this.setState({ dateRange: newUtcDateRange, selectedDateRange: undefined }, () => { - this.dataQueryManager.runQuery({ capabilities, dateRange: newUtcDateRange }); - }); - }} - fill - locale={enUS} /> - </FormGroup> - <FormGroup label="Datasource"> - <DatasourceSelect /> - </FormGroup> + )} </div> - </SplitterLayout> - ); - } -} + </ResizeSensor> + <div className="side-control"> + <FormGroup label="Show"> + <SegmentedControl + value={activeSegmentStat} + onValueChange={s => setActiveSegmentStat(s as SegmentStat)} + fill + options={[ + { + label: 'Size', + value: 'size', + }, + { + label: 'Count', + value: 'count', + }, + ]} + /> + </FormGroup> + <FormGroup label="Interval"> + <DateRangeInput3 + value={utcToLocalDateRange(dateRange)} + onChange={newDateRange => { + const newUtcDateRange = localToUtcDateRange(newDateRange); + if (!isNonNullRange(newUtcDateRange)) return; + setDateRange(newUtcDateRange); + }} + fill + locale={enUS} + /> + </FormGroup> + <FormGroup label="Datasource"> + <DatasourceSelect /> + </FormGroup> + </div> + </SplitterLayout> + ); +}; diff --git a/web-console/src/components/segment-timeline/stacked-bar-chart.tsx b/web-console/src/components/segment-timeline/stacked-bar-chart.tsx deleted file mode 100644 index 571c50f65a5..00000000000 --- a/web-console/src/components/segment-timeline/stacked-bar-chart.tsx +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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 type { AxisScale } from 'd3-axis'; -import { axisBottom, axisLeft } from 'd3-axis'; -import React, { useState } from 'react'; - -import { formatBytes, formatInteger } from '../../utils'; - -import { BarGroup } from './bar-group'; -import { ChartAxis } from './chart-axis'; -import type { BarUnitData, HoveredBarInfo, Margin, SegmentStat } from './common'; - -import './stacked-bar-chart.scss'; - -interface StackedBarChartProps { - svgWidth: number; - svgHeight: number; - margin: Margin; - shownSegmentStat: SegmentStat; - dataToRender: BarUnitData[]; - changeActiveDatasource: (e: string | undefined) => void; - xScale: AxisScale<Date>; - yScale: AxisScale<number>; - barWidth: number; -} - -export const StackedBarChart = React.forwardRef(function StackedBarChart( - props: StackedBarChartProps, - ref, -) { - const { - shownSegmentStat, - svgWidth, - svgHeight, - margin, - xScale, - yScale, - dataToRender, - changeActiveDatasource, - barWidth, - } = props; - const [hoverOn, setHoverOn] = useState<HoveredBarInfo>(); - - const formatTick = (n: number) => { - if (isNaN(n)) return ''; - if (shownSegmentStat === 'countData') { - return formatInteger(n); - } else { - return formatBytes(n); - } - }; - - const width = svgWidth - margin.left - margin.right; - const height = svgHeight - margin.top - margin.bottom; - - return ( - <div className="stacked-bar-chart" ref={ref as any}> - {hoverOn && ( - <div className="bar-chart-tooltip"> - <div>Datasource: {hoverOn.datasource}</div> - <div>Time: {hoverOn.xValue}</div> - <div> - {`${ - shownSegmentStat === 'countData' ? 'Daily total count:' : 'Daily total size:' - } ${formatTick(hoverOn.dailySize)}`} - </div> - <div> - {`${shownSegmentStat === 'countData' ? 'Count:' : 'Size:'} ${formatTick( - hoverOn.yValue, - )}`} - </div> - </div> - )} - <svg - width={svgWidth} - height={svgHeight} - viewBox={`0 0 ${svgWidth} ${svgHeight}`} - preserveAspectRatio="xMinYMin meet" - > - <g - transform={`translate(${margin.left}, ${margin.top})`} - onMouseLeave={() => setHoverOn(undefined)} - > - <ChartAxis - className="gridline-x" - transform="translate(0, 0)" - axis={axisLeft(yScale) - .ticks(5) - .tickSize(-width) - .tickFormat(() => '') - .tickSizeOuter(0)} - /> - <BarGroup - dataToRender={dataToRender} - changeActiveDatasource={changeActiveDatasource} - xScale={xScale} - yScale={yScale} - onHoverBar={(e: HoveredBarInfo) => setHoverOn(e)} - barWidth={barWidth} - /> - <ChartAxis - className="axis-x" - transform={`translate(0, ${height})`} - axis={axisBottom(xScale)} - /> - <ChartAxis - className="axis-y" - axis={axisLeft(yScale) - .ticks(5) - .tickFormat(e => formatTick(e))} - /> - {hoverOn && ( - <g - className="hovered-bar" - onClick={() => { - setHoverOn(undefined); - changeActiveDatasource(hoverOn.datasource); - }} - > - <rect - x={hoverOn.xCoordinate} - y={hoverOn.yCoordinate} - width={barWidth} - height={hoverOn.height} - /> - </g> - )} - </g> - </svg> - </div> - ); -}); diff --git a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx index 9b657622d89..5ee4d51a3a5 100644 --- a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx +++ b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx @@ -22,12 +22,12 @@ import React, { useState } from 'react'; import type { FormJsonTabs } from '../../components'; import { ExternalLink, FormJsonSelector, JsonInput, RuleEditor } from '../../components'; +import type { Rule } from '../../druid-models'; import type { Capabilities } from '../../helpers'; import { useQueryManager } from '../../hooks'; import { getLink } from '../../links'; import { Api } from '../../singletons'; import { filterMap, queryDruidSql, swapElements } from '../../utils'; -import type { Rule } from '../../utils/load-rule'; import { SnitchDialog } from '..'; import './retention-dialog.scss'; diff --git a/web-console/src/druid-models/index.ts b/web-console/src/druid-models/index.ts index dfeeeeaac83..82f14aab4e6 100644 --- a/web-console/src/druid-models/index.ts +++ b/web-console/src/druid-models/index.ts @@ -32,6 +32,7 @@ export * from './ingest-query-pattern/ingest-query-pattern'; export * from './ingestion-spec/ingestion-spec'; export * from './input-format/input-format'; export * from './input-source/input-source'; +export * from './load-rule/load-rule'; export * from './lookup-spec/lookup-spec'; export * from './metric-spec/metric-spec'; export * from './overlord-dynamic-config/overlord-dynamic-config'; diff --git a/web-console/src/utils/load-rule.ts b/web-console/src/druid-models/load-rule/load-rule.ts similarity index 98% rename from web-console/src/utils/load-rule.ts rename to web-console/src/druid-models/load-rule/load-rule.ts index a32422bbb6a..63bd162c272 100644 --- a/web-console/src/utils/load-rule.ts +++ b/web-console/src/druid-models/load-rule/load-rule.ts @@ -18,7 +18,7 @@ import { sum } from 'd3-array'; -import { deepMove, deepSet } from './object-change'; +import { deepMove, deepSet } from '../../utils'; export type RuleType = | 'loadForever' diff --git a/web-console/src/views/explore-view/models/stage.ts b/web-console/src/utils/stage.ts similarity index 80% rename from web-console/src/views/explore-view/models/stage.ts rename to web-console/src/utils/stage.ts index a1d01258f0a..7b65cfebd9c 100644 --- a/web-console/src/views/explore-view/models/stage.ts +++ b/web-console/src/utils/stage.ts @@ -16,6 +16,13 @@ * limitations under the License. */ +export interface Margin { + top: number; + right: number; + bottom: number; + left: number; +} + export class Stage { public readonly width: number; public readonly height: number; @@ -28,4 +35,11 @@ export class Stage { public equals(other: Stage | undefined): boolean { return Boolean(other && this.width === other.width && this.height === other.height); } + + public applyMargin(margin: Margin): Stage { + return new Stage( + this.width - margin.left - margin.right, + this.height - margin.top - margin.bottom, + ); + } } diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx index 8f89a0f1c38..a2bd3692346 100644 --- a/web-console/src/views/datasources-view/datasources-view.tsx +++ b/web-console/src/views/datasources-view/datasources-view.tsx @@ -51,8 +51,9 @@ import type { CompactionInfo, CompactionStatus, QueryWithContext, + Rule, } from '../../druid-models'; -import { formatCompactionInfo, zeroCompactionStatus } from '../../druid-models'; +import { formatCompactionInfo, RuleUtil, zeroCompactionStatus } from '../../druid-models'; import type { Capabilities, CapabilitiesMode } from '../../helpers'; import { STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table'; import { Api, AppToaster } from '../../singletons'; @@ -82,8 +83,6 @@ import { twoLines, } from '../../utils'; import type { BasicAction } from '../../utils/basic-action'; -import type { Rule } from '../../utils/load-rule'; -import { RuleUtil } from '../../utils/load-rule'; import './datasources-view.scss'; diff --git a/web-console/src/views/explore-view/components/module-pane/module-pane.tsx b/web-console/src/views/explore-view/components/module-pane/module-pane.tsx index b360effc87c..2a1f55c3d4a 100644 --- a/web-console/src/views/explore-view/components/module-pane/module-pane.tsx +++ b/web-console/src/views/explore-view/components/module-pane/module-pane.tsx @@ -20,8 +20,9 @@ import { ResizeSensor } from '@blueprintjs/core'; import type { QueryResult, SqlExpression, SqlQuery } from '@druid-toolkit/query'; import React, { useMemo, useState } from 'react'; +import { Stage } from '../../../../utils/stage'; import type { ParameterDefinition, ParameterValues, QuerySource } from '../../models'; -import { effectiveParameterDefault, Stage } from '../../models'; +import { effectiveParameterDefault } from '../../models'; import { ModuleRepository } from '../../module-repository/module-repository'; import { Issue } from '../issue/issue'; diff --git a/web-console/src/views/explore-view/models/index.ts b/web-console/src/views/explore-view/models/index.ts index 1e3c58ea09f..8938818acc2 100644 --- a/web-console/src/views/explore-view/models/index.ts +++ b/web-console/src/views/explore-view/models/index.ts @@ -22,4 +22,3 @@ export * from './measure'; export * from './measure-pattern'; export * from './parameter'; export * from './query-source'; -export * from './stage'; diff --git a/web-console/src/views/explore-view/module-repository/module-repository.ts b/web-console/src/views/explore-view/module-repository/module-repository.ts index d050a2779c5..fd9dd19f1b3 100644 --- a/web-console/src/views/explore-view/module-repository/module-repository.ts +++ b/web-console/src/views/explore-view/module-repository/module-repository.ts @@ -20,7 +20,8 @@ import type { IconName } from '@blueprintjs/icons'; import type { QueryResult, SqlExpression, SqlQuery } from '@druid-toolkit/query'; import type { CancelToken } from 'axios'; -import type { ParameterDefinition, QuerySource, Stage } from '../models'; +import type { Stage } from '../../../utils/stage'; +import type { ParameterDefinition, QuerySource } from '../models'; interface ModuleDefinition<P> { id: string; --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
