From 60395cbaa8430872ee29a75a917c5afe3f58419a Mon Sep 17 00:00:00 2001 From: Vadim Ogievetsky Date: Wed, 28 May 2025 15:34:29 -0700 Subject: [PATCH 1/7] Fix title typo and add tooltip --- web-console/src/dialogs/history-dialog/history-dialog.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web-console/src/dialogs/history-dialog/history-dialog.tsx b/web-console/src/dialogs/history-dialog/history-dialog.tsx index b17b8c2e93cb..f375f334ea56 100644 --- a/web-console/src/dialogs/history-dialog/history-dialog.tsx +++ b/web-console/src/dialogs/history-dialog/history-dialog.tsx @@ -71,6 +71,7 @@ export const HistoryDialog = React.memo(function HistoryDialog(props: HistoryDia id={i} key={i} title={`${auditInfo.comment || 'Change'} @ ${formattedTime}`} + data-tooltip={formattedTime} panel={ ({ label: auditInfo.comment || auditTime, value: normalizePayload(payload), From d16fcd47aa22587474f60c1aa4e7953fa08ea56d Mon Sep 17 00:00:00 2001 From: Vadim Ogievetsky Date: Wed, 28 May 2025 15:34:49 -0700 Subject: [PATCH 2/7] Always allow adding a tier --- .../components/rule-editor/rule-editor.tsx | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/web-console/src/components/rule-editor/rule-editor.tsx b/web-console/src/components/rule-editor/rule-editor.tsx index 8cea5321cfd7..d4e8b5a3eac2 100644 --- a/web-console/src/components/rule-editor/rule-editor.tsx +++ b/web-console/src/components/rule-editor/rule-editor.tsx @@ -62,10 +62,22 @@ export const RuleEditor = React.memo(function RuleEditor(props: RuleEditorProps) } function addTier() { - let newTierName = tiers[0]; + if (!rule.tieredReplicants) return; - if (rule.tieredReplicants) { - for (const tier of tiers) { + let newTierName: string | undefined; + + // Pick an exiting tier that is not assigned + for (const tier of tiers) { + if (rule.tieredReplicants[tier] === undefined) { + newTierName = tier; + break; + } + } + + // 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; @@ -73,7 +85,9 @@ export const RuleEditor = React.memo(function RuleEditor(props: RuleEditorProps) } } - onChange?.(RuleUtil.addTieredReplicant(rule, newTierName, 1)); + if (newTierName) { + onChange?.(RuleUtil.addTieredReplicant(rule, newTierName, 1)); + } } function renderTiers() { @@ -135,17 +149,10 @@ export const RuleEditor = React.memo(function RuleEditor(props: RuleEditorProps) function renderTierAdder() { if (!onChange) return; - const disabled = Object.keys(rule.tieredReplicants || {}).length >= Object.keys(tiers).length; return ( - From f7b446c18b54a2445f35c044907fbcb73dbac00d Mon Sep 17 00:00:00 2001 From: Vadim Ogievetsky Date: Thu, 29 May 2025 09:23:07 -0700 Subject: [PATCH 3/7] add tiered replicant --- .../components/rule-editor/rule-editor.tsx | 58 +++++--------- .../rule-editor/tiered-replicant.tsx | 76 +++++++++++++++++++ .../src/druid-models/load-rule/load-rule.ts | 6 +- 3 files changed, 98 insertions(+), 42 deletions(-) create mode 100644 web-console/src/components/rule-editor/tiered-replicant.tsx diff --git a/web-console/src/components/rule-editor/rule-editor.tsx b/web-console/src/components/rule-editor/rule-editor.tsx index d4e8b5a3eac2..fd521aa86deb 100644 --- a/web-console/src/components/rule-editor/rule-editor.tsx +++ b/web-console/src/components/rule-editor/rule-editor.tsx @@ -24,7 +24,6 @@ import { FormGroup, HTMLSelect, InputGroup, - NumericInput, Switch, } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; @@ -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']; @@ -103,45 +104,22 @@ export const RuleEditor = React.memo(function RuleEditor(props: RuleEditorProps) return ( - {tieredReplicantsList.map(([tier, replication]) => ( - - - - onChange?.(RuleUtil.renameTieredReplicants(rule, tier, e.target.value)) - } - > - - {tiers - .filter(t => t !== tier && !tieredReplicants[t]) - .map(t => ( - - ))} - - - { - if (isNaN(v)) return; - onChange?.(RuleUtil.addTieredReplicant(rule, tier, v)); - }} - min={0} - max={256} - /> - {onChange && + onChangeTier(value || '')} + suggestions={tiers.filter(t => t === tier || !usedTiers.includes(t))} + /> + + { + if (isNaN(v)) return; + onChangeReplication(v); + }} + min={0} + max={256} + /> + {onRemove &&
- - - + - - Open dropdown - - - - + + + + +
- - - + - - Open dropdown - - - - + + + + +
- - - + - - Open dropdown - - - - + + + + +
- - - + - - Open dropdown - - - - + + + + +
- - - + - - Open dropdown - - - - + + + + +
+
+
+ + + + + + +
+
+ +
+
+ +
+
+ + +
+
+ + +`; + +exports[`TieredReplicant matches snapshot with existing tier 1`] = ` +
+ +
+
+ + + + + + +
+
+ +
+
+ +
+
+ + +
+
+ +
+`; + +exports[`TieredReplicant matches snapshot with multiple used tiers 1`] = ` +
+ +
+
+ + + + + + +
+
+ +
+
+ +
+
+ + +
+
+ +
+`; + +exports[`TieredReplicant matches snapshot with non-existing tier 1`] = ` +
+ +
+
+ + + + + + +
+
+ +
+
+ +
+
+ + +
+
+ +
+`; + +exports[`TieredReplicant matches snapshot without remove button 1`] = ` +
+ +
+
+ + + + + + +
+
+ +
+
+ +
+
+ + +
+
+
+`; diff --git a/web-console/src/components/rule-editor/tiered-replicant.spec.tsx b/web-console/src/components/rule-editor/tiered-replicant.spec.tsx new file mode 100644 index 000000000000..d821b56336a1 --- /dev/null +++ b/web-console/src/components/rule-editor/tiered-replicant.spec.tsx @@ -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 = ( + {}} + onChangeReplication={() => {}} + onRemove={() => {}} + /> + ); + const { container } = render(tieredReplicant); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('matches snapshot with non-existing tier', () => { + const tieredReplicant = ( + {}} + onChangeReplication={() => {}} + onRemove={() => {}} + /> + ); + const { container } = render(tieredReplicant); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('matches snapshot when disabled', () => { + const tieredReplicant = ( + {}} + onChangeReplication={() => {}} + onRemove={() => {}} + /> + ); + const { container } = render(tieredReplicant); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('matches snapshot without remove button', () => { + const tieredReplicant = ( + {}} + onChangeReplication={() => {}} + onRemove={undefined} + /> + ); + const { container } = render(tieredReplicant); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('matches snapshot with multiple used tiers', () => { + const tieredReplicant = ( + {}} + onChangeReplication={() => {}} + onRemove={() => {}} + /> + ); + const { container } = render(tieredReplicant); + expect(container.firstChild).toMatchSnapshot(); + }); +}); diff --git a/web-console/src/dialogs/history-dialog/__snapshots__/history-dialog.spec.tsx.snap b/web-console/src/dialogs/history-dialog/__snapshots__/history-dialog.spec.tsx.snap index 50535a3a5d4c..a1094d387a03 100644 --- a/web-console/src/dialogs/history-dialog/__snapshots__/history-dialog.spec.tsx.snap +++ b/web-console/src/dialogs/history-dialog/__snapshots__/history-dialog.spec.tsx.snap @@ -85,11 +85,12 @@ exports[`HistoryDialog matches snapshot 1`] = ` aria-selected="true" class="bp5-tab" data-tab-id="0" + data-tooltip="2025-04-03 02:01:00" id="bp5-tab-title_undefined_0" role="tab" tabindex="0" > - test @ + test @ 2025-04-03 02:01:00
{ title="History" historyRecords={[ { - auditTime: 'test', + auditTime: '2025-04-03T02:01:00.000Z', auditInfo: { comment: 'test' }, payload: JSONBig.stringify({ name: 'test' }), }, { - auditTime: 'test', + auditTime: '2025-04-03T01:01:00.000Z', auditInfo: { comment: 'test' }, payload: JSONBig.stringify({ name: 'test' }), }, diff --git a/web-console/src/dialogs/history-dialog/history-dialog.tsx b/web-console/src/dialogs/history-dialog/history-dialog.tsx index f375f334ea56..4c99eefc1ed5 100644 --- a/web-console/src/dialogs/history-dialog/history-dialog.tsx +++ b/web-console/src/dialogs/history-dialog/history-dialog.tsx @@ -65,7 +65,7 @@ export const HistoryDialog = React.memo(function HistoryDialog(props: HistoryDia content = ( {historyRecords.map(({ auditInfo, auditTime, payload }, i) => { - const formattedTime = auditTime.replace('T', ' ').substring(0, auditTime.length - 5); + const formattedTime = auditTime.replace('T', ' ').substring(0, 19); return (
- - - + - - Open dropdown - - - - + + + + +
- - - - - Open dropdown - - - - + + + + +