mirror of
https://github.com/apache/druid.git
synced 2025-02-16 23:15:16 +00:00
Web console: fix a few console bugs (#16735)
* remove __time from min max query shortcut * fix scrolling in retention rules dialog * actions menus should have titles * change term * correctly name sort/shuffle
This commit is contained in:
parent
89066b72cf
commit
44b3f8e588
@ -23,7 +23,7 @@ import { ActionCell } from './action-cell';
|
||||
|
||||
describe('ActionCell', () => {
|
||||
it('matches snapshot', () => {
|
||||
const actionCell = <ActionCell onDetail={() => {}} actions={[]} />;
|
||||
const actionCell = <ActionCell onDetail={() => {}} actions={[]} menuTitle="item" />;
|
||||
const { container } = render(actionCell);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
@ -34,12 +34,13 @@ export const ACTION_COLUMN_WIDTH = 70;
|
||||
export interface ActionCellProps {
|
||||
onDetail?: () => void;
|
||||
disableDetail?: boolean;
|
||||
actions?: BasicAction[];
|
||||
actions: BasicAction[];
|
||||
menuTitle: string;
|
||||
}
|
||||
|
||||
export const ActionCell = React.memo(function ActionCell(props: ActionCellProps) {
|
||||
const { onDetail, disableDetail, actions } = props;
|
||||
const actionsMenu = actions ? basicActionsToMenu(actions) : null;
|
||||
const { onDetail, disableDetail, actions, menuTitle } = props;
|
||||
const actionsMenu = basicActionsToMenu(actions, menuTitle);
|
||||
|
||||
return (
|
||||
<div className="action-cell">
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -29,10 +29,26 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.rule-editor {
|
||||
.rule-form {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
.rule-form-content {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.rule-editor:not(:last-child) {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.json-input {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.no-rules-message {
|
||||
font-style: italic;
|
||||
}
|
||||
|
@ -114,6 +114,24 @@ ORDER BY 1`,
|
||||
setCurrentRules(swapElements(currentRules, index, index + direction));
|
||||
}
|
||||
|
||||
const defaultRuleRender =
|
||||
datasource !== CLUSTER_DEFAULT_FAKE_DATASOURCE ? (
|
||||
<FormGroup
|
||||
label={
|
||||
<>
|
||||
Cluster defaults (<a onClick={onEditDefaults}>edit</a>)
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>The cluster default rules are evaluated if none of the above rules match.</p>
|
||||
{currentTab === 'form' ? (
|
||||
defaultRules.map((rule, index) => <RuleEditor key={index} rule={rule} tiers={tiers} />)
|
||||
) : (
|
||||
<JsonInput value={defaultRules} />
|
||||
)}
|
||||
</FormGroup>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<SnitchDialog
|
||||
className="retention-dialog"
|
||||
@ -142,61 +160,51 @@ ORDER BY 1`,
|
||||
}}
|
||||
/>
|
||||
{currentTab === 'form' ? (
|
||||
<FormGroup>
|
||||
{currentRules.length ? (
|
||||
currentRules.map((rule, index) => (
|
||||
<RuleEditor
|
||||
key={index}
|
||||
rule={rule}
|
||||
tiers={tiers}
|
||||
onChange={r => changeRule(r, index)}
|
||||
onDelete={() => deleteRule(index)}
|
||||
moveUp={index > 0 ? () => moveRule(index, -1) : undefined}
|
||||
moveDown={index < currentRules.length - 1 ? () => moveRule(index, 1) : undefined}
|
||||
/>
|
||||
))
|
||||
) : datasource !== CLUSTER_DEFAULT_FAKE_DATASOURCE ? (
|
||||
<p className="no-rules-message">
|
||||
This datasource currently has no rules, it will use the cluster defaults.
|
||||
</p>
|
||||
) : undefined}
|
||||
<div>
|
||||
<Button
|
||||
icon={IconNames.PLUS}
|
||||
onClick={addRule}
|
||||
intent={currentRules.length ? undefined : Intent.PRIMARY}
|
||||
>
|
||||
New rule
|
||||
</Button>
|
||||
<div className="rule-form">
|
||||
<div className="rule-form-content">
|
||||
<FormGroup>
|
||||
{currentRules.length ? (
|
||||
currentRules.map((rule, index) => (
|
||||
<RuleEditor
|
||||
key={index}
|
||||
rule={rule}
|
||||
tiers={tiers}
|
||||
onChange={r => changeRule(r, index)}
|
||||
onDelete={() => deleteRule(index)}
|
||||
moveUp={index > 0 ? () => moveRule(index, -1) : undefined}
|
||||
moveDown={
|
||||
index < currentRules.length - 1 ? () => moveRule(index, 1) : undefined
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : datasource !== CLUSTER_DEFAULT_FAKE_DATASOURCE ? (
|
||||
<p className="no-rules-message">
|
||||
This datasource currently has no rules, it will use the cluster defaults.
|
||||
</p>
|
||||
) : undefined}
|
||||
<div>
|
||||
<Button
|
||||
icon={IconNames.PLUS}
|
||||
onClick={addRule}
|
||||
intent={currentRules.length ? undefined : Intent.PRIMARY}
|
||||
>
|
||||
New rule
|
||||
</Button>
|
||||
</div>
|
||||
</FormGroup>
|
||||
{defaultRuleRender && <Divider />}
|
||||
{defaultRuleRender}
|
||||
</div>
|
||||
</FormGroup>
|
||||
</div>
|
||||
) : (
|
||||
<JsonInput
|
||||
value={currentRules}
|
||||
onChange={setCurrentRules}
|
||||
setError={setJsonError}
|
||||
height="100%"
|
||||
/>
|
||||
)}
|
||||
{datasource !== CLUSTER_DEFAULT_FAKE_DATASOURCE && (
|
||||
<>
|
||||
<Divider />
|
||||
<FormGroup
|
||||
label={
|
||||
<>
|
||||
Cluster defaults (<a onClick={onEditDefaults}>edit</a>)
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p>The cluster default rules are evaluated if none of the above rules match.</p>
|
||||
{currentTab === 'form' ? (
|
||||
defaultRules.map((rule, index) => (
|
||||
<RuleEditor key={index} rule={rule} tiers={tiers} />
|
||||
))
|
||||
) : (
|
||||
<JsonInput value={defaultRules} />
|
||||
)}
|
||||
</FormGroup>
|
||||
<JsonInput
|
||||
value={currentRules}
|
||||
onChange={setCurrentRules}
|
||||
setError={setJsonError}
|
||||
height="100%"
|
||||
/>
|
||||
{defaultRuleRender}
|
||||
</>
|
||||
)}
|
||||
</SnitchDialog>
|
||||
|
@ -27,8 +27,7 @@ describe('Stages', () => {
|
||||
|
||||
describe('#getByPartitionCountersForStage', () => {
|
||||
it('works for input', () => {
|
||||
expect(STAGES.getByPartitionCountersForStage(STAGES.stages[2], 'input'))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(STAGES.getByPartitionCountersForStage(STAGES.stages[2], 'in')).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"index": 0,
|
||||
@ -45,8 +44,7 @@ describe('Stages', () => {
|
||||
});
|
||||
|
||||
it('works for output', () => {
|
||||
expect(STAGES.getByPartitionCountersForStage(STAGES.stages[2], 'output'))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(STAGES.getByPartitionCountersForStage(STAGES.stages[2], 'out')).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"index": 0,
|
||||
|
@ -22,8 +22,10 @@ import { deleteKeys, filterMap, oneOf, zeroDivide } from '../../utils';
|
||||
import type { InputFormat } from '../input-format/input-format';
|
||||
import type { InputSource } from '../input-source/input-source';
|
||||
|
||||
const SORT_WEIGHT = 0.5;
|
||||
const READING_INPUT_WITH_SORT_WEIGHT = 1 - SORT_WEIGHT;
|
||||
const SHUFFLE_WEIGHT = 0.5;
|
||||
const READING_INPUT_WITH_SHUFFLE_WEIGHT = 1 - SHUFFLE_WEIGHT;
|
||||
|
||||
export type InOut = 'in' | 'out';
|
||||
|
||||
export type StageInput =
|
||||
| {
|
||||
@ -261,13 +263,13 @@ export class Stages {
|
||||
return Stages.stageType(stage) !== 'segmentGenerator';
|
||||
}
|
||||
|
||||
stageHasSort(stage: StageDefinition): boolean {
|
||||
stageHasShuffle(stage: StageDefinition): boolean {
|
||||
if (!this.stageHasOutput(stage)) return false;
|
||||
return Boolean(stage.sort);
|
||||
return Boolean(stage.sort) || this.hasCounterForStage(stage, 'shuffle');
|
||||
}
|
||||
|
||||
stageOutputCounterName(stage: StageDefinition): ChannelCounterName {
|
||||
return this.stageHasSort(stage) ? 'shuffle' : 'output';
|
||||
stageFinalCounterName(stage: StageDefinition): ChannelCounterName {
|
||||
return this.stageHasShuffle(stage) ? 'shuffle' : 'output';
|
||||
}
|
||||
|
||||
overallProgress(): number {
|
||||
@ -286,12 +288,14 @@ export class Stages {
|
||||
switch (stage.phase) {
|
||||
case 'READING_INPUT':
|
||||
return (
|
||||
(this.stageHasSort(stage) ? READING_INPUT_WITH_SORT_WEIGHT : 1) *
|
||||
(this.stageHasShuffle(stage) ? READING_INPUT_WITH_SHUFFLE_WEIGHT : 1) *
|
||||
this.readingInputPhaseProgress(stage)
|
||||
);
|
||||
|
||||
case 'POST_READING':
|
||||
return READING_INPUT_WITH_SORT_WEIGHT + SORT_WEIGHT * this.postReadingPhaseProgress(stage);
|
||||
return (
|
||||
READING_INPUT_WITH_SHUFFLE_WEIGHT + SHUFFLE_WEIGHT * this.postReadingPhaseProgress(stage)
|
||||
);
|
||||
|
||||
case 'RESULTS_READY':
|
||||
case 'FINISHED':
|
||||
@ -415,7 +419,7 @@ export class Stages {
|
||||
}
|
||||
|
||||
getTotalOutputForStage(stage: StageDefinition, field: 'frames' | 'rows' | 'bytes'): number {
|
||||
return this.getTotalCounterForStage(stage, this.stageOutputCounterName(stage), field);
|
||||
return this.getTotalCounterForStage(stage, this.stageFinalCounterName(stage), field);
|
||||
}
|
||||
|
||||
getSortProgressForStage(stage: StageDefinition): number {
|
||||
@ -445,7 +449,7 @@ export class Stages {
|
||||
|
||||
const channelCounters = definition.input.map((_, i) => `input${i}` as ChannelCounterName);
|
||||
if (this.stageHasOutput(stage)) channelCounters.push('output');
|
||||
if (this.stageHasSort(stage)) channelCounters.push('shuffle');
|
||||
if (this.stageHasShuffle(stage)) channelCounters.push('shuffle');
|
||||
return channelCounters;
|
||||
}
|
||||
|
||||
@ -479,9 +483,9 @@ export class Stages {
|
||||
|
||||
getPartitionChannelCounterNamesForStage(
|
||||
stage: StageDefinition,
|
||||
type: 'input' | 'output',
|
||||
inOut: InOut,
|
||||
): ChannelCounterName[] {
|
||||
if (type === 'input') {
|
||||
if (inOut === 'in') {
|
||||
const { input, broadcast } = stage.definition;
|
||||
return filterMap(input, (input, i) =>
|
||||
input.type === 'stage' && !broadcast?.includes(i)
|
||||
@ -489,31 +493,28 @@ export class Stages {
|
||||
: undefined,
|
||||
);
|
||||
} else {
|
||||
return [this.stageOutputCounterName(stage)];
|
||||
return [this.stageFinalCounterName(stage)];
|
||||
}
|
||||
}
|
||||
|
||||
getByPartitionCountersForStage(
|
||||
stage: StageDefinition,
|
||||
type: 'input' | 'output',
|
||||
): SimpleWideCounter[] {
|
||||
const counterNames = this.getPartitionChannelCounterNamesForStage(stage, type);
|
||||
getByPartitionCountersForStage(stage: StageDefinition, inOut: InOut): SimpleWideCounter[] {
|
||||
const counterNames = this.getPartitionChannelCounterNamesForStage(stage, inOut);
|
||||
if (!counterNames.length) return [];
|
||||
|
||||
if (!this.hasCounterForStage(stage, counterNames[0])) return [];
|
||||
const stageCounters = this.getCountersForStage(stage);
|
||||
|
||||
const { partitionCount } = stage;
|
||||
const partitionNumber =
|
||||
type === 'output'
|
||||
? partitionCount
|
||||
: max(stageCounters, stageCounter =>
|
||||
max(counterNames, counterName => {
|
||||
const channelCounter = stageCounter[counterName];
|
||||
if (channelCounter?.type !== 'channel') return 0;
|
||||
return channelCounter.rows?.length || 0;
|
||||
}),
|
||||
);
|
||||
let partitionNumber = max(stageCounters, stageCounter =>
|
||||
max(counterNames, counterName => {
|
||||
const channelCounter = stageCounter[counterName];
|
||||
if (channelCounter?.type !== 'channel') return 0;
|
||||
return channelCounter.rows?.length || 0;
|
||||
}),
|
||||
);
|
||||
|
||||
if (inOut === 'out') {
|
||||
partitionNumber = Math.max(partitionNumber || 0, stage.partitionCount || 0);
|
||||
}
|
||||
|
||||
if (!partitionNumber) return [];
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
import type { IconName, Intent } from '@blueprintjs/core';
|
||||
import { Menu, MenuItem } from '@blueprintjs/core';
|
||||
import { Menu, MenuDivider, MenuItem } from '@blueprintjs/core';
|
||||
import type { JSX } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
@ -29,10 +29,14 @@ export interface BasicAction {
|
||||
disabledReason?: string;
|
||||
}
|
||||
|
||||
export function basicActionsToMenu(basicActions: BasicAction[]): JSX.Element | undefined {
|
||||
export function basicActionsToMenu(
|
||||
basicActions: BasicAction[],
|
||||
title?: string,
|
||||
): JSX.Element | undefined {
|
||||
if (!basicActions.length) return;
|
||||
return (
|
||||
<Menu>
|
||||
{title && <MenuDivider title={title} />}
|
||||
{basicActions.map(({ icon, title, intent, onAction, disabledReason }, i) => (
|
||||
<MenuItem
|
||||
key={i}
|
||||
|
@ -1212,7 +1212,7 @@ GROUP BY 1, 2`;
|
||||
num_segments !== num_zero_replica_segments
|
||||
? `Fully ${descriptor}`
|
||||
: undefined,
|
||||
hasZeroReplicationRule ? `${percentZeroReplica}% async only` : '',
|
||||
hasZeroReplicationRule ? `${percentZeroReplica}% deep storage only` : '',
|
||||
).join(', ')}{' '}
|
||||
({segmentsEl})
|
||||
</span>
|
||||
@ -1228,7 +1228,7 @@ GROUP BY 1, 2`;
|
||||
{numAvailableSegments ? '\u25cf' : '\u25cb'}
|
||||
</span>
|
||||
{`${percentAvailable}% ${descriptor}${
|
||||
hasZeroReplicationRule ? `, ${percentZeroReplica}% async only` : ''
|
||||
hasZeroReplicationRule ? `, ${percentZeroReplica}% deep storage only` : ''
|
||||
}`}{' '}
|
||||
({segmentsEl})
|
||||
</span>
|
||||
@ -1614,6 +1614,7 @@ GROUP BY 1, 2`;
|
||||
}}
|
||||
disableDetail={unused}
|
||||
actions={datasourceActions}
|
||||
menuTitle={datasource}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -464,6 +464,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
|
||||
this.onDetail(original);
|
||||
}}
|
||||
actions={lookupActions}
|
||||
menuTitle={lookupId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -886,6 +886,7 @@ END AS "time_span"`,
|
||||
this.onDetail(id, datasource);
|
||||
}}
|
||||
actions={this.getSegmentActions(id, datasource)}
|
||||
menuTitle={id}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -656,7 +656,7 @@ ORDER BY
|
||||
const { worker } = value;
|
||||
const disabled = worker.version === '';
|
||||
const workerActions = this.getWorkerActions(worker.host, disabled);
|
||||
return <ActionCell actions={workerActions} />;
|
||||
return <ActionCell actions={workerActions} menuTitle={worker.host} />;
|
||||
},
|
||||
Aggregated: () => '',
|
||||
},
|
||||
|
@ -898,6 +898,7 @@ export class SupervisorsView extends React.PureComponent<
|
||||
<ActionCell
|
||||
onDetail={() => this.onSupervisorDetail(row.original)}
|
||||
actions={supervisorActions}
|
||||
menuTitle={id}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -505,6 +505,7 @@ ORDER BY
|
||||
<ActionCell
|
||||
onDetail={() => this.onTaskDetail(row.original)}
|
||||
actions={this.getTaskActions(id, datasource, status, type, true)}
|
||||
menuTitle={id}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -339,7 +339,8 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
|
||||
F.max(C('__time')).as('max_time'),
|
||||
])
|
||||
.changeGroupByExpressions([])
|
||||
.changeWhereExpression(getWhere(true)),
|
||||
.changeWhereExpression(getWhere(true))
|
||||
.removeColumnFromWhere('__time'),
|
||||
true,
|
||||
);
|
||||
}}
|
||||
|
@ -32,6 +32,7 @@ import type {
|
||||
ClusterBy,
|
||||
CounterName,
|
||||
Execution,
|
||||
InOut,
|
||||
SegmentGenerationProgressFields,
|
||||
SimpleWideCounter,
|
||||
StageDefinition,
|
||||
@ -179,9 +180,9 @@ export const ExecutionStagesPane = React.memo(function ExecutionStagesPane(
|
||||
const phaseIsWorking = oneOf(phase, 'NEW', 'READING_INPUT', 'POST_READING');
|
||||
return (
|
||||
<div className="execution-stage-detail-pane">
|
||||
{detailedCountersForPartitions(stage, 'input', phase === 'READING_INPUT')}
|
||||
{detailedCountersForPartitions(stage, 'in', phase === 'READING_INPUT')}
|
||||
{detailedCountersForWorkers(stage)}
|
||||
{detailedCountersForPartitions(stage, 'output', phaseIsWorking)}
|
||||
{detailedCountersForPartitions(stage, 'out', phaseIsWorking)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -320,15 +321,15 @@ export const ExecutionStagesPane = React.memo(function ExecutionStagesPane(
|
||||
|
||||
function detailedCountersForPartitions(
|
||||
stage: StageDefinition,
|
||||
type: 'input' | 'output',
|
||||
inOut: InOut,
|
||||
inProgress: boolean,
|
||||
) {
|
||||
const wideCounters = stages.getByPartitionCountersForStage(stage, type);
|
||||
const wideCounters = stages.getByPartitionCountersForStage(stage, inOut);
|
||||
if (!wideCounters.length) return;
|
||||
|
||||
const counterNames: ChannelCounterName[] = stages.getPartitionChannelCounterNamesForStage(
|
||||
stage,
|
||||
type,
|
||||
inOut,
|
||||
);
|
||||
|
||||
const bracesRows: Record<ChannelCounterName, string[]> = {} as any;
|
||||
@ -349,7 +350,7 @@ export const ExecutionStagesPane = React.memo(function ExecutionStagesPane(
|
||||
showPagination={wideCounters.length > MAX_DETAIL_ROWS}
|
||||
columns={[
|
||||
{
|
||||
Header: `${capitalizeFirst(type)} partitions` + (inProgress ? '*' : ''),
|
||||
Header: `${capitalizeFirst(inOut)} partitions` + (inProgress ? '*' : ''),
|
||||
id: 'partition',
|
||||
accessor: d => d.index,
|
||||
className: 'padded',
|
||||
|
Loading…
x
Reference in New Issue
Block a user