diff --git a/web-console/src/components/rule-editor/__snapshots__/rule-editor.spec.tsx.snap b/web-console/src/components/rule-editor/__snapshots__/rule-editor.spec.tsx.snap index 3db574c8fe0..58a3de27e45 100644 --- a/web-console/src/components/rule-editor/__snapshots__/rule-editor.spec.tsx.snap +++ b/web-console/src/components/rule-editor/__snapshots__/rule-editor.spec.tsx.snap @@ -174,6 +174,66 @@ exports[`RuleEditor matches snapshot no tier in rule 1`] = `
+ +
+ + + + + Open dropdown + + + + +
- -
- - - - - Open dropdown - - - - -
+
+ + + + + Open dropdown + + + + +
-
- - - - - Open dropdown - - - - -
+
+ + + + + Open dropdown + + + + +
-
- - - - - Open dropdown - - - - -
+
+ + + + + Open dropdown + + + + +
-
- - - - - Open dropdown - - - - -
+
+ + + + + Open dropdown + + + + +
-
- - - - - Open dropdown - - - - -
- { - if (isNaN(v)) return; - onChange(RuleUtil.addTieredReplicant(rule, tier, v)); - }} - min={0} - max={256} - /> @@ -110,14 +98,24 @@ export const RuleEditor = React.memo(function RuleEditor(props: RuleEditorProps) {tiers .filter(t => t !== tier && !tieredReplicants[t]) - .map(t => { - return ( - - ); - })} + .map(t => ( + + ))} + + { + if (isNaN(v)) return; + onChange(RuleUtil.addTieredReplicant(rule, tier, v)); + }} + min={0} + max={256} + /> + + + +
@@ -310,6 +342,51 @@ exports[`RetentionDialog matches snapshot 1`] = `
+ +
+ + + + + Open dropdown + + + + +
- -
- - - - - Open dropdown - - - - -
-
-
- -
-
diff --git a/web-console/src/dialogs/retention-dialog/retention-dialog.scss b/web-console/src/dialogs/retention-dialog/retention-dialog.scss index 7fa00021ada..df521abbd11 100644 --- a/web-console/src/dialogs/retention-dialog/retention-dialog.scss +++ b/web-console/src/dialogs/retention-dialog/retention-dialog.scss @@ -22,9 +22,13 @@ &.#{$bp-ns}-dialog { top: 5%; width: 750px; + height: 80vh; } .#{$bp-ns}-dialog-body { + display: flex; + flex-direction: column; + .rule-editor { margin-bottom: 15px; } diff --git a/web-console/src/dialogs/retention-dialog/retention-dialog.spec.tsx b/web-console/src/dialogs/retention-dialog/retention-dialog.spec.tsx index afee143a06e..1e5f09fd00a 100644 --- a/web-console/src/dialogs/retention-dialog/retention-dialog.spec.tsx +++ b/web-console/src/dialogs/retention-dialog/retention-dialog.spec.tsx @@ -19,6 +19,8 @@ import { render } from '@testing-library/react'; import React from 'react'; +import { Capabilities } from '../../helpers'; + import { RetentionDialog } from './retention-dialog'; describe('RetentionDialog', () => { @@ -35,7 +37,7 @@ describe('RetentionDialog', () => { }, ]} defaultRules={[{ tieredReplicants: { _default_tier: 2 }, type: 'loadForever' }]} - tiers={['tier1', 'tier2']} + capabilities={Capabilities.FULL} onEditDefaults={() => {}} onCancel={() => {}} onSave={() => {}} diff --git a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx index 723bf23b9be..4d50d84c3e1 100644 --- a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx +++ b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx @@ -16,43 +16,75 @@ * limitations under the License. */ -import { Button, Divider, FormGroup } from '@blueprintjs/core'; +import { Button, Divider, FormGroup, Intent } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import React, { useState } from 'react'; -import { ExternalLink, RuleEditor } from '../../components'; +import type { FormJsonTabs } from '../../components'; +import { ExternalLink, FormJsonSelector, JsonInput, RuleEditor } from '../../components'; +import type { Capabilities } from '../../helpers'; import { useQueryManager } from '../../hooks'; import { getLink } from '../../links'; import { Api } from '../../singletons'; -import { swapElements } from '../../utils'; +import { filterMap, queryDruidSql, swapElements } from '../../utils'; import type { Rule } from '../../utils/load-rule'; import { RuleUtil } from '../../utils/load-rule'; import { SnitchDialog } from '..'; import './retention-dialog.scss'; +const CLUSTER_DEFAULT_FAKE_DATASOURCE = '_default'; + export interface RetentionDialogProps { datasource: string; rules: Rule[]; defaultRules: Rule[]; - tiers: string[]; - onEditDefaults: () => void; - onCancel: () => void; - onSave: (datasource: string, newRules: Rule[], comment: string) => void | Promise; + capabilities: Capabilities; + onEditDefaults(): void; + onCancel(): void; + onSave(datasource: string, newRules: Rule[], comment: string): void | Promise; } export const RetentionDialog = React.memo(function RetentionDialog(props: RetentionDialogProps) { - const { datasource, onCancel, onEditDefaults, rules, defaultRules, tiers } = props; + const { datasource, onCancel, onEditDefaults, rules, defaultRules, capabilities } = props; + const [currentTab, setCurrentTab] = useState('form'); const [currentRules, setCurrentRules] = useState(props.rules); + const [jsonError, setJsonError] = useState(); + + const [tiersState] = useQueryManager({ + initQuery: capabilities, + processQuery: async capabilities => { + if (capabilities.hasSql()) { + const sqlResp = await queryDruidSql<{ tier: string }>({ + query: `SELECT "tier" +FROM "sys"."servers" +WHERE "server_type" = 'historical' +GROUP BY 1 +ORDER BY 1`, + }); + + return sqlResp.map(d => d.tier); + } else if (capabilities.hasCoordinatorAccess()) { + const allServiceResp = await Api.instance.get('/druid/coordinator/v1/servers?simple'); + return filterMap(allServiceResp.data, (s: any) => + s.type === 'historical' ? s.tier : undefined, + ); + } else { + throw new Error(`must have sql or coordinator access`); + } + }, + }); + + const tiers = tiersState.data || []; const [historyQueryState] = useQueryManager({ + initQuery: props.datasource, processQuery: async datasource => { const historyResp = await Api.instance.get( `/druid/coordinator/v1/rules/${Api.encodePath(datasource)}/history`, ); return historyResp.data; }, - initQuery: props.datasource, }); const historyRecords = historyQueryState.data || []; @@ -108,10 +140,10 @@ export const RetentionDialog = React.memo(function RetentionDialog(props: Retent return ( setCurrentRules(rules)} onSave={saveHandler} @@ -125,21 +157,41 @@ export const RetentionDialog = React.memo(function RetentionDialog(props: Retent .

- - {currentRules.length ? ( - currentRules.map(renderRule) - ) : datasource !== '_default' ? ( -

- This datasource currently has no rules, it will use the cluster defaults. -

- ) : undefined} -
- -
-
- {datasource !== '_default' && ( + { + setJsonError(undefined); + setCurrentTab(t); + }} + /> + {currentTab === 'form' ? ( + + {currentRules.length ? ( + currentRules.map(renderRule) + ) : datasource !== CLUSTER_DEFAULT_FAKE_DATASOURCE ? ( +

+ This datasource currently has no rules, it will use the cluster defaults. +

+ ) : undefined} +
+ +
+
+ ) : ( + + )} + {datasource !== CLUSTER_DEFAULT_FAKE_DATASOURCE && ( <> diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx index 16a6aadf744..58a5a13e9d1 100644 --- a/web-console/src/views/datasources-view/datasources-view.tsx +++ b/web-console/src/views/datasources-view/datasources-view.tsx @@ -250,8 +250,6 @@ export interface DatasourcesViewState { datasourceFilter: Filter[]; datasourcesAndDefaultRulesState: QueryState; - tiersState: QueryState; - showUnused: boolean; retentionDialogOpenOn?: RetentionDialogOpenOn; compactionDialogOpenOn?: CompactionConfigDialogOpenOn; @@ -349,8 +347,6 @@ ORDER BY 1`; DatasourcesAndDefaultRules >; - private readonly tiersQueryManager: QueryManager; - constructor(props: DatasourcesViewProps, context: any) { super(props, context); @@ -363,8 +359,6 @@ ORDER BY 1`; datasourceFilter, datasourcesAndDefaultRulesState: QueryState.INIT, - tiersState: QueryState.INIT, - showUnused: false, useUnuseAction: 'unuse', useUnuseInterval: '', @@ -525,26 +519,11 @@ ORDER BY 1`; }); }, }); - - this.tiersQueryManager = new QueryManager({ - processQuery: async capabilities => { - if (capabilities.hasCoordinatorAccess()) { - const tiersResp = await Api.instance.get('/druid/coordinator/v1/tiers'); - return tiersResp.data; - } else { - throw new Error(`must have coordinator access`); - } - }, - onStateChange: tiersState => { - this.setState({ tiersState }); - }, - }); } private readonly refresh = (auto: boolean): void => { if (auto && hasPopoverOpen()) return; this.datasourceQueryManager.rerunLastQuery(auto); - this.tiersQueryManager.rerunLastQuery(auto); }; private fetchDatasourceData() { @@ -554,14 +533,11 @@ ORDER BY 1`; } componentDidMount(): void { - const { capabilities } = this.props; this.fetchDatasourceData(); - this.tiersQueryManager.runQuery(capabilities); } componentWillUnmount(): void { this.datasourceQueryManager.terminate(); - this.tiersQueryManager.terminate(); } renderUnuseAction() { @@ -964,7 +940,8 @@ ORDER BY 1`; } private renderRetentionDialog(): JSX.Element | undefined { - const { retentionDialogOpenOn, tiersState, datasourcesAndDefaultRulesState } = this.state; + const { capabilities } = this.props; + const { retentionDialogOpenOn, datasourcesAndDefaultRulesState } = this.state; const defaultRules = datasourcesAndDefaultRulesState.data?.defaultRules; if (!retentionDialogOpenOn || !defaultRules) return; @@ -972,7 +949,7 @@ ORDER BY 1`; this.setState({ retentionDialogOpenOn: undefined })}