Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web console: fix a few console bugs #16735

Merged
merged 5 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down
7 changes: 4 additions & 3 deletions web-console/src/components/action-cell/action-cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion web-console/src/dialogs/retention-dialog/retention-dialog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
112 changes: 60 additions & 52 deletions web-console/src/dialogs/retention-dialog/retention-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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>
Expand Down
6 changes: 2 additions & 4 deletions web-console/src/druid-models/stages/stages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
59 changes: 30 additions & 29 deletions web-console/src/druid-models/stages/stages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
| {
Expand Down Expand Up @@ -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 {
Expand All @@ -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':
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -479,41 +483,38 @@ 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)
? (`input${i}` as ChannelCounterName)
: 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 [];

Expand Down
8 changes: 6 additions & 2 deletions web-console/src/utils/basic-action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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}
Expand Down
5 changes: 3 additions & 2 deletions web-console/src/views/datasources-view/datasources-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down Expand Up @@ -1614,6 +1614,7 @@ GROUP BY 1, 2`;
}}
disableDetail={unused}
actions={datasourceActions}
menuTitle={datasource}
/>
);
},
Expand Down
1 change: 1 addition & 0 deletions web-console/src/views/lookups-view/lookups-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
this.onDetail(original);
}}
actions={lookupActions}
menuTitle={lookupId}
/>
);
},
Expand Down
1 change: 1 addition & 0 deletions web-console/src/views/segments-view/segments-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,7 @@ END AS "time_span"`,
this.onDetail(id, datasource);
}}
actions={this.getSegmentActions(id, datasource)}
menuTitle={id}
/>
);
},
Expand Down
Loading
Loading