Skip to content
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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

89 changes: 37 additions & 52 deletions web-console/src/components/rule-editor/rule-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
FormGroup,
HTMLSelect,
InputGroup,
NumericInput,
Switch,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
Expand All @@ -35,6 +34,8 @@ import { RuleUtil } from '../../druid-models';
import { durationSanitizer } from '../../utils';
import { SuggestibleInput } from '../suggestible-input/suggestible-input';

import { TieredReplicant } from './tiered-replicant';

import './rule-editor.scss';

const PERIOD_SUGGESTIONS: string[] = ['P1D', 'P7D', 'P1M', 'P1Y', 'P1000Y'];
Expand Down Expand Up @@ -62,18 +63,32 @@ export const RuleEditor = React.memo(function RuleEditor(props: RuleEditorProps)
}

function addTier() {
let newTierName = tiers[0];
if (!rule.tieredReplicants) return;

let newTierName: string | undefined;

// Pick an existing tier that is not assigned
for (const tier of tiers) {
if (rule.tieredReplicants[tier] === undefined) {
newTierName = tier;
break;
}
}

if (rule.tieredReplicants) {
for (const tier of tiers) {
// If no such tier exists, pick a new tier name
if (!newTierName) {
for (let i = 1; i < 100; i++) {
const tier = `tier${i}`;
if (rule.tieredReplicants[tier] === undefined) {
newTierName = tier;
break;
}
}
}

onChange?.(RuleUtil.addTieredReplicant(rule, newTierName, 1));
if (newTierName) {
onChange?.(RuleUtil.addTieredReplicant(rule, newTierName, 1));
}
}

function renderTiers() {
Expand All @@ -89,63 +104,33 @@ export const RuleEditor = React.memo(function RuleEditor(props: RuleEditorProps)

return (
<FormGroup>
{tieredReplicantsList.map(([tier, replication]) => (
<ControlGroup key={tier}>
<Button minimal disabled={disabled} style={{ pointerEvents: 'none' }}>
Tier:
</Button>
<HTMLSelect
fill
value={tier}
disabled={disabled}
onChange={(e: any) =>
onChange?.(RuleUtil.renameTieredReplicants(rule, tier, e.target.value))
}
>
<option key={tier} value={tier}>
{tier}
</option>
{tiers
.filter(t => t !== tier && !tieredReplicants[t])
.map(t => (
<option key={t} value={t}>
{t}
</option>
))}
</HTMLSelect>
<Button minimal disabled={disabled} style={{ pointerEvents: 'none' }}>
Replicants:
</Button>
<NumericInput
value={replication}
disabled={disabled}
onValueChange={(v: number) => {
if (isNaN(v)) return;
onChange?.(RuleUtil.addTieredReplicant(rule, tier, v));
}}
min={0}
max={256}
/>
{onChange && <Button onClick={() => removeTier(tier)} icon={IconNames.TRASH} />}
</ControlGroup>
{tieredReplicantsList.map(([tier, replication], i) => (
<TieredReplicant
key={i}
tier={tier}
replication={replication}
tiers={tiers}
usedTiers={Object.keys(tieredReplicants)}
disabled={disabled}
onChangeTier={newTier =>
onChange?.(RuleUtil.renameTieredReplicant(rule, tier, newTier))
}
onChangeReplication={value =>
onChange?.(RuleUtil.addTieredReplicant(rule, tier, value))
}
onRemove={onChange ? () => removeTier(tier) : undefined}
/>
))}
</FormGroup>
);
}

function renderTierAdder() {
if (!onChange) return;
const disabled = Object.keys(rule.tieredReplicants || {}).length >= Object.keys(tiers).length;

return (
<FormGroup>
<Button
onClick={addTier}
minimal
icon={IconNames.PLUS}
disabled={disabled}
data-tooltip={disabled ? 'There are no tiers left to assign' : undefined}
>
<Button onClick={addTier} minimal icon={IconNames.PLUS} disabled={disabled}>
Add historical tier replication
</Button>
</FormGroup>
Expand Down
108 changes: 108 additions & 0 deletions web-console/src/components/rule-editor/tiered-replicant.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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 { render } from '@testing-library/react';

import { TieredReplicant } from './tiered-replicant';

describe('TieredReplicant', () => {
it('matches snapshot with existing tier', () => {
const tieredReplicant = (
<TieredReplicant
tier="test1"
replication={2}
tiers={['test1', 'test2', 'test3']}
usedTiers={['test1']}
disabled={false}
onChangeTier={() => {}}
onChangeReplication={() => {}}
onRemove={() => {}}
/>
);
const { container } = render(tieredReplicant);
expect(container.firstChild).toMatchSnapshot();
});

it('matches snapshot with non-existing tier', () => {
const tieredReplicant = (
<TieredReplicant
tier="nonexist"
replication={1}
tiers={['test1', 'test2', 'test3']}
usedTiers={['nonexist']}
disabled={false}
onChangeTier={() => {}}
onChangeReplication={() => {}}
onRemove={() => {}}
/>
);
const { container } = render(tieredReplicant);
expect(container.firstChild).toMatchSnapshot();
});

it('matches snapshot when disabled', () => {
const tieredReplicant = (
<TieredReplicant
tier="test1"
replication={3}
tiers={['test1', 'test2', 'test3']}
usedTiers={['test1']}
disabled
onChangeTier={() => {}}
onChangeReplication={() => {}}
onRemove={() => {}}
/>
);
const { container } = render(tieredReplicant);
expect(container.firstChild).toMatchSnapshot();
});

it('matches snapshot without remove button', () => {
const tieredReplicant = (
<TieredReplicant
tier="test2"
replication={1}
tiers={['test1', 'test2', 'test3']}
usedTiers={['test2']}
disabled={false}
onChangeTier={() => {}}
onChangeReplication={() => {}}
onRemove={undefined}
/>
);
const { container } = render(tieredReplicant);
expect(container.firstChild).toMatchSnapshot();
});

it('matches snapshot with multiple used tiers', () => {
const tieredReplicant = (
<TieredReplicant
tier="test3"
replication={5}
tiers={['test1', 'test2', 'test3']}
usedTiers={['test1', 'test2']}
disabled={false}
onChangeTier={() => {}}
onChangeReplication={() => {}}
onRemove={() => {}}
/>
);
const { container } = render(tieredReplicant);
expect(container.firstChild).toMatchSnapshot();
});
});
76 changes: 76 additions & 0 deletions web-console/src/components/rule-editor/tiered-replicant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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 { Button, ControlGroup, NumericInput } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import React from 'react';

import { SuggestibleInput } from '../suggestible-input/suggestible-input';

export interface TieredReplicantProps {
tier: string;
replication: number;
onChangeTier: (newTier: string) => void;
onChangeReplication: (value: number) => void;
tiers: string[];
usedTiers: string[];
disabled: boolean;
onRemove?: () => void;
}

export const TieredReplicant = React.memo(function TieredReplicant(props: TieredReplicantProps) {
const {
tier,
replication,
tiers,
usedTiers,
disabled,
onChangeReplication,
onChangeTier,
onRemove,
} = props;

return (
<ControlGroup className="tiered-replicant">
<Button minimal disabled={disabled} style={{ pointerEvents: 'none' }}>
Tier:
</Button>
<SuggestibleInput
fill
value={tier}
disabled={disabled}
onValueChange={value => onChangeTier(value || '')}
suggestions={tiers.filter(t => t === tier || !usedTiers.includes(t))}
/>
<Button minimal disabled={disabled} style={{ pointerEvents: 'none' }}>
Replicants:
</Button>
<NumericInput
value={replication}
disabled={disabled}
onValueChange={(v: number) => {
if (isNaN(v)) return;
onChangeReplication(v);
}}
min={0}
max={256}
/>
{onRemove && <Button onClick={onRemove} icon={IconNames.TRASH} />}
</ControlGroup>
);
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SuggestionMenu matches snapshot 1`] = `
exports[`SuggestionMenu matches snapshot when empty 1`] = `
<ul
class="suggestion-menu bp5-menu"
role="menu"
>
<li
class=""
role="none"
>
<a
aria-disabled="true"
class="bp5-menu-item bp5-disabled"
role="menuitem"
tabindex="-1"
>
<div
class="bp5-text-overflow-ellipsis bp5-fill"
>
(empty)
</div>
</a>
</li>
</ul>
`;

exports[`SuggestionMenu matches snapshot with something 1`] = `
<ul
class="suggestion-menu bp5-menu"
role="menu"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ import { render } from '@testing-library/react';
import { SuggestionMenu } from './suggestion-menu';

describe('SuggestionMenu', () => {
it('matches snapshot', () => {
it('matches snapshot when empty', () => {
const arrayInput = <SuggestionMenu suggestions={[]} onSuggest={() => {}} />;

const { container } = render(arrayInput);
expect(container.firstChild).toMatchSnapshot();
});

it('matches snapshot with something', () => {
const arrayInput = (
<SuggestionMenu
suggestions={[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const SuggestionMenu = React.memo(function SuggestionMenu(props: Suggesti
);
}
})}
{suggestions.length === 0 && <MenuItem text="(empty)" disabled />}
</Menu>
);
});
Loading