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:
Vadim Ogievetsky 2024-07-17 14:51:17 -07:00 committed by GitHub
parent 89066b72cf
commit 44b3f8e588
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 818 additions and 775 deletions

View File

@ -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();
});

View File

@ -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">

View File

@ -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;
}

View File

@ -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>

View File

@ -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,

View File

@ -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 [];

View File

@ -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}

View File

@ -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'}&nbsp;
</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}
/>
);
},

View File

@ -464,6 +464,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
this.onDetail(original);
}}
actions={lookupActions}
menuTitle={lookupId}
/>
);
},

View File

@ -886,6 +886,7 @@ END AS "time_span"`,
this.onDetail(id, datasource);
}}
actions={this.getSegmentActions(id, datasource)}
menuTitle={id}
/>
);
},

View File

@ -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: () => '',
},

View File

@ -898,6 +898,7 @@ export class SupervisorsView extends React.PureComponent<
<ActionCell
onDetail={() => this.onSupervisorDetail(row.original)}
actions={supervisorActions}
menuTitle={id}
/>
);
},

View File

@ -505,6 +505,7 @@ ORDER BY
<ActionCell
onDetail={() => this.onTaskDetail(row.original)}
actions={this.getTaskActions(id, datasource, status, type, true)}
menuTitle={id}
/>
);
},

View File

@ -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,
);
}}

View File

@ -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',