| /* |
| * 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 { FormGroup, InputGroup, Intent, MenuItem, Switch } from '@blueprintjs/core'; |
| import { IconNames } from '@blueprintjs/icons'; |
| import classNames from 'classnames'; |
| import { SqlQuery, SqlTableRef } from 'druid-query-toolkit'; |
| import React from 'react'; |
| import ReactTable, { Filter } from 'react-table'; |
| |
| import { |
| ACTION_COLUMN_ID, |
| ACTION_COLUMN_LABEL, |
| ACTION_COLUMN_WIDTH, |
| ActionCell, |
| ActionIcon, |
| BracedText, |
| MoreButton, |
| RefreshButton, |
| SegmentTimeline, |
| TableColumnSelector, |
| ViewControlBar, |
| } from '../../components'; |
| import { AsyncActionDialog, CompactionDialog, RetentionDialog } from '../../dialogs'; |
| import { DatasourceTableActionDialog } from '../../dialogs/datasource-table-action-dialog/datasource-table-action-dialog'; |
| import { |
| CompactionConfig, |
| CompactionStatus, |
| formatCompactionConfigAndStatus, |
| zeroCompactionStatus, |
| } from '../../druid-models'; |
| import { Api, AppToaster } from '../../singletons'; |
| import { |
| addFilter, |
| Capabilities, |
| CapabilitiesMode, |
| compact, |
| countBy, |
| deepGet, |
| formatBytes, |
| formatInteger, |
| formatMillions, |
| formatPercent, |
| getDruidErrorMessage, |
| LocalStorageKeys, |
| lookupBy, |
| pluralIfNeeded, |
| queryDruidSql, |
| QueryManager, |
| QueryState, |
| } from '../../utils'; |
| import { BasicAction } from '../../utils/basic-action'; |
| import { Rule, RuleUtil } from '../../utils/load-rule'; |
| import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array'; |
| |
| import './datasource-view.scss'; |
| |
| const tableColumns: Record<CapabilitiesMode, string[]> = { |
| 'full': [ |
| 'Datasource name', |
| 'Availability', |
| 'Availability detail', |
| 'Total data size', |
| 'Segment size', |
| 'Segment granularity', |
| 'Total rows', |
| 'Avg. row size', |
| 'Replicated size', |
| 'Compaction', |
| '% Compacted', |
| 'Left to be compacted', |
| 'Retention', |
| ACTION_COLUMN_LABEL, |
| ], |
| 'no-sql': [ |
| 'Datasource name', |
| 'Availability', |
| 'Availability detail', |
| 'Total data size', |
| 'Compaction', |
| '% Compacted', |
| 'Left to be compacted', |
| 'Retention', |
| ACTION_COLUMN_LABEL, |
| ], |
| 'no-proxy': [ |
| 'Datasource name', |
| 'Availability', |
| 'Availability detail', |
| 'Total data size', |
| 'Segment size', |
| 'Segment granularity', |
| 'Total rows', |
| 'Avg. row size', |
| 'Replicated size', |
| ACTION_COLUMN_LABEL, |
| ], |
| }; |
| |
| const DEFAULT_RULES_KEY = '_default'; |
| |
| function formatLoadDrop(segmentsToLoad: number, segmentsToDrop: number): string { |
| const loadDrop: string[] = []; |
| if (segmentsToLoad) { |
| loadDrop.push(`${pluralIfNeeded(segmentsToLoad, 'segment')} to load`); |
| } |
| if (segmentsToDrop) { |
| loadDrop.push(`${pluralIfNeeded(segmentsToDrop, 'segment')} to drop`); |
| } |
| return loadDrop.join(', ') || 'No segments to load/drop'; |
| } |
| |
| const formatTotalDataSize = formatBytes; |
| const formatSegmentRows = formatMillions; |
| const formatTotalRows = formatInteger; |
| const formatAvgRowSize = formatInteger; |
| const formatReplicatedSize = formatBytes; |
| const formatLeftToBeCompacted = formatBytes; |
| |
| function twoLines(line1: string, line2: string) { |
| return ( |
| <> |
| {line1} |
| <br /> |
| {line2} |
| </> |
| ); |
| } |
| |
| function progress(done: number, awaiting: number): number { |
| const d = done + awaiting; |
| if (!d) return 0; |
| return done / d; |
| } |
| |
| const PERCENT_BRACES = [formatPercent(1)]; |
| |
| interface DatasourceQueryResultRow { |
| readonly datasource: string; |
| readonly num_segments: number; |
| readonly num_segments_to_load: number; |
| readonly num_segments_to_drop: number; |
| readonly minute_aligned_segments: number; |
| readonly hour_aligned_segments: number; |
| readonly day_aligned_segments: number; |
| readonly month_aligned_segments: number; |
| readonly year_aligned_segments: number; |
| readonly total_data_size: number; |
| readonly replicated_size: number; |
| readonly min_segment_rows: number; |
| readonly avg_segment_rows: number; |
| readonly max_segment_rows: number; |
| readonly total_rows: number; |
| readonly avg_row_size: number; |
| } |
| |
| function makeEmptyDatasourceQueryResultRow(datasource: string): DatasourceQueryResultRow { |
| return { |
| datasource, |
| num_segments: 0, |
| num_segments_to_load: 0, |
| num_segments_to_drop: 0, |
| minute_aligned_segments: 0, |
| hour_aligned_segments: 0, |
| day_aligned_segments: 0, |
| month_aligned_segments: 0, |
| year_aligned_segments: 0, |
| total_data_size: 0, |
| replicated_size: 0, |
| min_segment_rows: 0, |
| avg_segment_rows: 0, |
| max_segment_rows: 0, |
| total_rows: 0, |
| avg_row_size: 0, |
| }; |
| } |
| |
| function segmentGranularityCountsToRank(row: DatasourceQueryResultRow): number { |
| return ( |
| Number(Boolean(row.num_segments)) + |
| Number(Boolean(row.minute_aligned_segments)) + |
| Number(Boolean(row.hour_aligned_segments)) + |
| Number(Boolean(row.day_aligned_segments)) + |
| Number(Boolean(row.month_aligned_segments)) + |
| Number(Boolean(row.year_aligned_segments)) |
| ); |
| } |
| |
| interface Datasource extends DatasourceQueryResultRow { |
| readonly rules: Rule[]; |
| readonly compactionConfig?: CompactionConfig; |
| readonly compactionStatus?: CompactionStatus; |
| readonly unused?: boolean; |
| } |
| |
| function makeUnusedDatasource(datasource: string): Datasource { |
| return { ...makeEmptyDatasourceQueryResultRow(datasource), rules: [], unused: true }; |
| } |
| |
| interface DatasourcesAndDefaultRules { |
| readonly datasources: Datasource[]; |
| readonly defaultRules: Rule[]; |
| } |
| |
| interface RetentionDialogOpenOn { |
| readonly datasource: string; |
| readonly rules: Rule[]; |
| } |
| |
| interface CompactionDialogOpenOn { |
| readonly datasource: string; |
| readonly compactionConfig: CompactionConfig; |
| } |
| |
| export interface DatasourcesViewProps { |
| goToQuery: (initSql: string) => void; |
| goToTask: (datasource?: string, openDialog?: string) => void; |
| goToSegments: (datasource: string, onlyUnavailable?: boolean) => void; |
| capabilities: Capabilities; |
| initDatasource?: string; |
| } |
| |
| export interface DatasourcesViewState { |
| datasourceFilter: Filter[]; |
| datasourcesAndDefaultRulesState: QueryState<DatasourcesAndDefaultRules>; |
| |
| tiersState: QueryState<string[]>; |
| |
| showUnused: boolean; |
| retentionDialogOpenOn?: RetentionDialogOpenOn; |
| compactionDialogOpenOn?: CompactionDialogOpenOn; |
| datasourceToMarkAsUnusedAllSegmentsIn?: string; |
| datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn?: string; |
| killDatasource?: string; |
| datasourceToMarkSegmentsByIntervalIn?: string; |
| useUnuseAction: 'use' | 'unuse'; |
| useUnuseInterval: string; |
| showForceCompact: boolean; |
| hiddenColumns: LocalStorageBackedArray<string>; |
| showSegmentTimeline: boolean; |
| |
| datasourceTableActionDialogId?: string; |
| actions: BasicAction[]; |
| } |
| |
| interface DatasourceQuery { |
| capabilities: Capabilities; |
| hiddenColumns: LocalStorageBackedArray<string>; |
| showUnused: boolean; |
| } |
| |
| export class DatasourcesView extends React.PureComponent< |
| DatasourcesViewProps, |
| DatasourcesViewState |
| > { |
| static UNUSED_COLOR = '#0a1500'; |
| static FULLY_AVAILABLE_COLOR = '#57d500'; |
| static PARTIALLY_AVAILABLE_COLOR = '#ffbf00'; |
| |
| static query(hiddenColumns: LocalStorageBackedArray<string>) { |
| const columns = compact( |
| [ |
| hiddenColumns.exists('Datasource name') && `datasource`, |
| (hiddenColumns.exists('Availability') || hiddenColumns.exists('Segment granularity')) && |
| `COUNT(*) FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS num_segments`, |
| (hiddenColumns.exists('Availability') || hiddenColumns.exists('Availability detail')) && [ |
| `COUNT(*) FILTER (WHERE is_published = 1 AND is_overshadowed = 0 AND is_available = 0) AS num_segments_to_load`, |
| `COUNT(*) FILTER (WHERE is_available = 1 AND NOT ((is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1)) AS num_segments_to_drop`, |
| ], |
| hiddenColumns.exists('Total data size') && |
| `SUM("size") FILTER (WHERE is_published = 1 AND is_overshadowed = 0) AS total_data_size`, |
| hiddenColumns.exists('Segment size') && [ |
| `MIN("num_rows") FILTER (WHERE is_published = 1 AND is_overshadowed = 0) AS min_segment_rows`, |
| `AVG("num_rows") FILTER (WHERE is_published = 1 AND is_overshadowed = 0) AS avg_segment_rows`, |
| `MAX("num_rows") FILTER (WHERE is_published = 1 AND is_overshadowed = 0) AS max_segment_rows`, |
| ], |
| hiddenColumns.exists('Segment granularity') && [ |
| `COUNT(*) FILTER (WHERE ((is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AND "start" LIKE '%:00.000Z' AND "end" LIKE '%:00.000Z') AS minute_aligned_segments`, |
| `COUNT(*) FILTER (WHERE ((is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AND "start" LIKE '%:00:00.000Z' AND "end" LIKE '%:00:00.000Z') AS hour_aligned_segments`, |
| `COUNT(*) FILTER (WHERE ((is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AND "start" LIKE '%T00:00:00.000Z' AND "end" LIKE '%T00:00:00.000Z') AS day_aligned_segments`, |
| `COUNT(*) FILTER (WHERE ((is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AND "start" LIKE '%-01T00:00:00.000Z' AND "end" LIKE '%-01T00:00:00.000Z') AS month_aligned_segments`, |
| `COUNT(*) FILTER (WHERE ((is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AND "start" LIKE '%-01-01T00:00:00.000Z' AND "end" LIKE '%-01-01T00:00:00.000Z') AS year_aligned_segments`, |
| ], |
| hiddenColumns.exists('Total rows') && |
| `SUM("num_rows") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS total_rows`, |
| hiddenColumns.exists('Avg. row size') && |
| `CASE WHEN SUM("num_rows") FILTER (WHERE is_published = 1 AND is_overshadowed = 0) <> 0 THEN (SUM("size") FILTER (WHERE is_published = 1 AND is_overshadowed = 0) / SUM("num_rows") FILTER (WHERE is_published = 1 AND is_overshadowed = 0)) ELSE 0 END AS avg_row_size`, |
| hiddenColumns.exists('Replicated size') && |
| `SUM("size" * "num_replicas") FILTER (WHERE is_published = 1 AND is_overshadowed = 0) AS replicated_size`, |
| ].flat(), |
| ); |
| |
| if (!columns.length) { |
| columns.push(`datasource`); |
| } |
| |
| return `SELECT |
| ${columns.join(',\n')} |
| FROM sys.segments |
| GROUP BY 1 |
| ORDER BY 1`; |
| } |
| |
| static formatRules(rules: Rule[]): string { |
| if (rules.length === 0) { |
| return 'No rules'; |
| } else if (rules.length <= 2) { |
| return rules.map(RuleUtil.ruleToString).join(', '); |
| } else { |
| return `${RuleUtil.ruleToString(rules[0])} +${rules.length - 1} more rules`; |
| } |
| } |
| |
| private readonly datasourceQueryManager: QueryManager< |
| DatasourceQuery, |
| DatasourcesAndDefaultRules |
| >; |
| |
| private readonly tiersQueryManager: QueryManager<Capabilities, string[]>; |
| |
| constructor(props: DatasourcesViewProps, context: any) { |
| super(props, context); |
| |
| const datasourceFilter: Filter[] = []; |
| if (props.initDatasource) { |
| datasourceFilter.push({ id: 'datasource', value: `"${props.initDatasource}"` }); |
| } |
| |
| this.state = { |
| datasourceFilter, |
| datasourcesAndDefaultRulesState: QueryState.INIT, |
| |
| tiersState: QueryState.INIT, |
| |
| showUnused: false, |
| useUnuseAction: 'unuse', |
| useUnuseInterval: '', |
| showForceCompact: false, |
| hiddenColumns: new LocalStorageBackedArray<string>( |
| LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION, |
| ), |
| showSegmentTimeline: false, |
| |
| actions: [], |
| }; |
| |
| this.datasourceQueryManager = new QueryManager({ |
| processQuery: async ( |
| { capabilities, hiddenColumns, showUnused }, |
| _cancelToken, |
| setIntermediateQuery, |
| ) => { |
| let datasources: DatasourceQueryResultRow[]; |
| if (capabilities.hasSql()) { |
| const query = DatasourcesView.query(hiddenColumns); |
| setIntermediateQuery(query); |
| datasources = await queryDruidSql({ query }); |
| } else if (capabilities.hasCoordinatorAccess()) { |
| const datasourcesResp = await Api.instance.get( |
| '/druid/coordinator/v1/datasources?simple', |
| ); |
| const loadstatusResp = await Api.instance.get('/druid/coordinator/v1/loadstatus?simple'); |
| const loadstatus = loadstatusResp.data; |
| datasources = datasourcesResp.data.map( |
| (d: any): DatasourceQueryResultRow => { |
| const totalDataSize = deepGet(d, 'properties.segments.size') || -1; |
| const segmentsToLoad = Number(loadstatus[d.name] || 0); |
| const availableSegments = Number(deepGet(d, 'properties.segments.count')); |
| const numSegments = availableSegments + segmentsToLoad; |
| return { |
| datasource: d.name, |
| num_segments: numSegments, |
| num_segments_to_load: segmentsToLoad, |
| num_segments_to_drop: 0, |
| minute_aligned_segments: -1, |
| hour_aligned_segments: -1, |
| day_aligned_segments: -1, |
| month_aligned_segments: -1, |
| year_aligned_segments: -1, |
| replicated_size: -1, |
| total_data_size: totalDataSize, |
| min_segment_rows: -1, |
| avg_segment_rows: -1, |
| max_segment_rows: -1, |
| total_rows: -1, |
| avg_row_size: -1, |
| }; |
| }, |
| ); |
| } else { |
| throw new Error(`must have SQL or coordinator access`); |
| } |
| |
| if (!capabilities.hasCoordinatorAccess()) { |
| return { |
| datasources: datasources.map(ds => ({ ...ds, rules: [] })), |
| defaultRules: [], |
| }; |
| } |
| |
| const seen = countBy(datasources, x => x.datasource); |
| |
| let unused: string[] = []; |
| if (showUnused) { |
| const unusedResp = await Api.instance.get<string[]>( |
| '/druid/coordinator/v1/metadata/datasources?includeUnused', |
| ); |
| unused = unusedResp.data.filter(d => !seen[d]); |
| } |
| |
| const rulesResp = await Api.instance.get<Record<string, Rule[]>>( |
| '/druid/coordinator/v1/rules', |
| ); |
| const rules = rulesResp.data; |
| |
| const compactionConfigsResp = await Api.instance.get<{ |
| compactionConfigs: CompactionConfig[]; |
| }>('/druid/coordinator/v1/config/compaction'); |
| const compactionConfigs = lookupBy( |
| compactionConfigsResp.data.compactionConfigs || [], |
| c => c.dataSource, |
| ); |
| |
| const compactionStatusesResp = await Api.instance.get<{ latestStatus: CompactionStatus[] }>( |
| '/druid/coordinator/v1/compaction/status', |
| ); |
| const compactionStatuses = lookupBy( |
| compactionStatusesResp.data.latestStatus || [], |
| c => c.dataSource, |
| ); |
| |
| return { |
| datasources: datasources.concat(unused.map(makeUnusedDatasource)).map(ds => { |
| return { |
| ...ds, |
| rules: rules[ds.datasource] || [], |
| compactionConfig: compactionConfigs[ds.datasource], |
| compactionStatus: compactionStatuses[ds.datasource], |
| }; |
| }), |
| defaultRules: rules[DEFAULT_RULES_KEY] || [], |
| }; |
| }, |
| onStateChange: datasourcesAndDefaultRulesState => { |
| this.setState({ |
| datasourcesAndDefaultRulesState, |
| }); |
| }, |
| }); |
| |
| 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: any): void => { |
| this.datasourceQueryManager.rerunLastQuery(auto); |
| this.tiersQueryManager.rerunLastQuery(auto); |
| }; |
| |
| private fetchDatasourceData() { |
| const { capabilities } = this.props; |
| const { hiddenColumns, showUnused } = this.state; |
| this.datasourceQueryManager.runQuery({ capabilities, hiddenColumns, showUnused }); |
| } |
| |
| componentDidMount(): void { |
| const { capabilities } = this.props; |
| this.fetchDatasourceData(); |
| this.tiersQueryManager.runQuery(capabilities); |
| } |
| |
| componentWillUnmount(): void { |
| this.datasourceQueryManager.terminate(); |
| this.tiersQueryManager.terminate(); |
| } |
| |
| renderUnuseAction() { |
| const { datasourceToMarkAsUnusedAllSegmentsIn } = this.state; |
| if (!datasourceToMarkAsUnusedAllSegmentsIn) return; |
| |
| return ( |
| <AsyncActionDialog |
| action={async () => { |
| const resp = await Api.instance.delete( |
| `/druid/coordinator/v1/datasources/${Api.encodePath( |
| datasourceToMarkAsUnusedAllSegmentsIn, |
| )}`, |
| {}, |
| ); |
| return resp.data; |
| }} |
| confirmButtonText="Mark as unused all segments" |
| successText="All segments in datasource have been marked as unused" |
| failText="Failed to mark as unused all segments in datasource" |
| intent={Intent.DANGER} |
| onClose={() => { |
| this.setState({ datasourceToMarkAsUnusedAllSegmentsIn: undefined }); |
| }} |
| onSuccess={() => { |
| this.fetchDatasourceData(); |
| }} |
| > |
| <p> |
| {`Are you sure you want to mark as unused all segments in '${datasourceToMarkAsUnusedAllSegmentsIn}'?`} |
| </p> |
| </AsyncActionDialog> |
| ); |
| } |
| |
| renderUseAction() { |
| const { datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn } = this.state; |
| if (!datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn) return; |
| |
| return ( |
| <AsyncActionDialog |
| action={async () => { |
| const resp = await Api.instance.post( |
| `/druid/coordinator/v1/datasources/${Api.encodePath( |
| datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn, |
| )}`, |
| {}, |
| ); |
| return resp.data; |
| }} |
| confirmButtonText="Mark as used all segments" |
| successText="All non-overshadowed segments in datasource have been marked as used" |
| failText="Failed to mark as used all non-overshadowed segments in datasource" |
| intent={Intent.PRIMARY} |
| onClose={() => { |
| this.setState({ datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn: undefined }); |
| }} |
| onSuccess={() => { |
| this.fetchDatasourceData(); |
| }} |
| > |
| <p>{`Are you sure you want to mark as used all non-overshadowed segments in '${datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn}'?`}</p> |
| </AsyncActionDialog> |
| ); |
| } |
| |
| renderUseUnuseActionByInterval() { |
| const { datasourceToMarkSegmentsByIntervalIn, useUnuseAction, useUnuseInterval } = this.state; |
| if (!datasourceToMarkSegmentsByIntervalIn) return; |
| const isUse = useUnuseAction === 'use'; |
| const usedWord = isUse ? 'used' : 'unused'; |
| return ( |
| <AsyncActionDialog |
| action={async () => { |
| if (!useUnuseInterval) return; |
| const param = isUse ? 'markUsed' : 'markUnused'; |
| const resp = await Api.instance.post( |
| `/druid/coordinator/v1/datasources/${Api.encodePath( |
| datasourceToMarkSegmentsByIntervalIn, |
| )}/${Api.encodePath(param)}`, |
| { |
| interval: useUnuseInterval, |
| }, |
| ); |
| return resp.data; |
| }} |
| confirmButtonText={`Mark as ${usedWord} segments in the interval`} |
| confirmButtonDisabled={!/.\/./.test(useUnuseInterval)} |
| successText={`Segments in the interval in datasource have been marked as ${usedWord}`} |
| failText={`Failed to mark as ${usedWord} segments in the interval in datasource`} |
| intent={Intent.PRIMARY} |
| onClose={() => { |
| this.setState({ datasourceToMarkSegmentsByIntervalIn: undefined }); |
| }} |
| onSuccess={() => { |
| this.fetchDatasourceData(); |
| }} |
| > |
| <p>{`Please select the interval in which you want to mark segments as ${usedWord} in '${datasourceToMarkSegmentsByIntervalIn}'?`}</p> |
| <FormGroup> |
| <InputGroup |
| value={useUnuseInterval} |
| onChange={(e: any) => { |
| const v = e.target.value; |
| this.setState({ useUnuseInterval: v.toUpperCase() }); |
| }} |
| placeholder="2018-01-01T00:00:00/2018-01-03T00:00:00" |
| /> |
| </FormGroup> |
| </AsyncActionDialog> |
| ); |
| } |
| |
| renderKillAction() { |
| const { killDatasource } = this.state; |
| if (!killDatasource) return; |
| |
| return ( |
| <AsyncActionDialog |
| action={async () => { |
| const resp = await Api.instance.delete( |
| `/druid/coordinator/v1/datasources/${Api.encodePath( |
| killDatasource, |
| )}?kill=true&interval=1000/3000`, |
| {}, |
| ); |
| return resp.data; |
| }} |
| confirmButtonText="Permanently delete unused segments" |
| successText="Kill task was issued. Unused segments in datasource will be deleted" |
| failText="Failed submit kill task" |
| intent={Intent.DANGER} |
| onClose={() => { |
| this.setState({ killDatasource: undefined }); |
| }} |
| onSuccess={() => { |
| this.fetchDatasourceData(); |
| }} |
| warningChecks={[ |
| `I understand that this operation will delete all metadata about the unused segments of ${killDatasource} and removes them from deep storage.`, |
| 'I understand that this operation cannot be undone.', |
| ]} |
| > |
| <p> |
| {`Are you sure you want to permanently delete unused segments in '${killDatasource}'?`} |
| </p> |
| <p>This action is not reversible and the data deleted will be lost.</p> |
| </AsyncActionDialog> |
| ); |
| } |
| |
| renderBulkDatasourceActions() { |
| const { goToQuery, capabilities } = this.props; |
| const lastDatasourcesQuery = this.datasourceQueryManager.getLastIntermediateQuery(); |
| |
| return ( |
| <MoreButton |
| altExtra={ |
| <MenuItem |
| icon={IconNames.COMPRESSED} |
| text="Force compaction run (debug)" |
| intent={Intent.DANGER} |
| onClick={() => { |
| this.setState({ showForceCompact: true }); |
| }} |
| /> |
| } |
| > |
| {capabilities.hasSql() && ( |
| <MenuItem |
| icon={IconNames.APPLICATION} |
| text="View SQL query for table" |
| disabled={!lastDatasourcesQuery} |
| onClick={() => { |
| if (!lastDatasourcesQuery) return; |
| goToQuery(lastDatasourcesQuery); |
| }} |
| /> |
| )} |
| <MenuItem |
| icon={IconNames.EDIT} |
| text="Edit default retention rules" |
| onClick={this.editDefaultRules} |
| /> |
| </MoreButton> |
| ); |
| } |
| |
| renderForceCompactAction() { |
| const { showForceCompact } = this.state; |
| if (!showForceCompact) return; |
| |
| return ( |
| <AsyncActionDialog |
| action={async () => { |
| const resp = await Api.instance.post(`/druid/coordinator/v1/compaction/compact`, {}); |
| return resp.data; |
| }} |
| confirmButtonText="Force compaction run" |
| successText="Out of band compaction run has been initiated" |
| failText="Could not force compaction" |
| intent={Intent.DANGER} |
| onClose={() => { |
| this.setState({ showForceCompact: false }); |
| }} |
| > |
| <p>Are you sure you want to force a compaction run?</p> |
| <p>This functionality only exists for debugging and testing reasons.</p> |
| <p>If you are running it in production you are doing something wrong.</p> |
| </AsyncActionDialog> |
| ); |
| } |
| |
| private readonly saveRules = async (datasource: string, rules: Rule[], comment: string) => { |
| try { |
| await Api.instance.post(`/druid/coordinator/v1/rules/${Api.encodePath(datasource)}`, rules, { |
| headers: { |
| 'X-Druid-Author': 'console', |
| 'X-Druid-Comment': comment, |
| }, |
| }); |
| } catch (e) { |
| AppToaster.show({ |
| message: `Failed to submit retention rules: ${getDruidErrorMessage(e)}`, |
| intent: Intent.DANGER, |
| }); |
| return; |
| } |
| |
| AppToaster.show({ |
| message: 'Retention rules submitted successfully', |
| intent: Intent.SUCCESS, |
| }); |
| this.fetchDatasourceData(); |
| }; |
| |
| private readonly editDefaultRules = () => { |
| this.setState({ retentionDialogOpenOn: undefined }); |
| setTimeout(() => { |
| this.setState(state => { |
| const datasourcesAndDefaultRules = state.datasourcesAndDefaultRulesState.data; |
| if (!datasourcesAndDefaultRules) return {}; |
| |
| return { |
| retentionDialogOpenOn: { |
| datasource: '_default', |
| rules: datasourcesAndDefaultRules.defaultRules, |
| }, |
| }; |
| }); |
| }, 50); |
| }; |
| |
| private readonly saveCompaction = async (compactionConfig: any) => { |
| if (!compactionConfig) return; |
| try { |
| await Api.instance.post(`/druid/coordinator/v1/config/compaction`, compactionConfig); |
| this.setState({ compactionDialogOpenOn: undefined }); |
| this.fetchDatasourceData(); |
| } catch (e) { |
| AppToaster.show({ |
| message: getDruidErrorMessage(e), |
| intent: Intent.DANGER, |
| }); |
| } |
| }; |
| |
| private readonly deleteCompaction = () => { |
| const { compactionDialogOpenOn } = this.state; |
| if (!compactionDialogOpenOn) return; |
| const datasource = compactionDialogOpenOn.datasource; |
| AppToaster.show({ |
| message: `Are you sure you want to delete ${datasource}'s compaction?`, |
| intent: Intent.DANGER, |
| action: { |
| text: 'Confirm', |
| onClick: async () => { |
| try { |
| await Api.instance.delete( |
| `/druid/coordinator/v1/config/compaction/${Api.encodePath(datasource)}`, |
| ); |
| this.setState({ compactionDialogOpenOn: undefined }, () => this.fetchDatasourceData()); |
| } catch (e) { |
| AppToaster.show({ |
| message: getDruidErrorMessage(e), |
| intent: Intent.DANGER, |
| }); |
| } |
| }, |
| }, |
| }); |
| }; |
| |
| private toggleUnused(showUnused: boolean) { |
| this.setState({ showUnused: !showUnused }, () => { |
| if (showUnused) return; |
| this.fetchDatasourceData(); |
| }); |
| } |
| |
| getDatasourceActions( |
| datasource: string, |
| unused: boolean, |
| rules: Rule[], |
| compactionConfig: CompactionConfig, |
| ): BasicAction[] { |
| const { goToQuery, goToTask, capabilities } = this.props; |
| |
| const goToActions: BasicAction[] = []; |
| |
| if (capabilities.hasSql()) { |
| goToActions.push({ |
| icon: IconNames.APPLICATION, |
| title: 'Query with SQL', |
| onAction: () => goToQuery(SqlQuery.create(SqlTableRef.create(datasource)).toString()), |
| }); |
| } |
| |
| goToActions.push({ |
| icon: IconNames.GANTT_CHART, |
| title: 'Go to tasks', |
| onAction: () => goToTask(datasource), |
| }); |
| |
| if (!capabilities.hasCoordinatorAccess()) { |
| return goToActions; |
| } |
| |
| if (unused) { |
| return [ |
| { |
| icon: IconNames.EXPORT, |
| title: 'Mark as used all segments', |
| |
| onAction: () => |
| this.setState({ |
| datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn: datasource, |
| }), |
| }, |
| { |
| icon: IconNames.TRASH, |
| title: 'Delete segments (issue kill task)', |
| intent: Intent.DANGER, |
| onAction: () => this.setState({ killDatasource: datasource }), |
| }, |
| ]; |
| } else { |
| return goToActions.concat([ |
| { |
| icon: IconNames.AUTOMATIC_UPDATES, |
| title: 'Edit retention rules', |
| onAction: () => { |
| this.setState({ |
| retentionDialogOpenOn: { |
| datasource, |
| rules, |
| }, |
| }); |
| }, |
| }, |
| { |
| icon: IconNames.REFRESH, |
| title: 'Mark as used all segments (will lead to reapplying retention rules)', |
| onAction: () => |
| this.setState({ |
| datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn: datasource, |
| }), |
| }, |
| { |
| icon: IconNames.COMPRESSED, |
| title: 'Edit compaction configuration', |
| onAction: () => { |
| this.setState({ |
| compactionDialogOpenOn: { |
| datasource, |
| compactionConfig, |
| }, |
| }); |
| }, |
| }, |
| { |
| icon: IconNames.EXPORT, |
| title: 'Mark as used segments by interval', |
| |
| onAction: () => |
| this.setState({ |
| datasourceToMarkSegmentsByIntervalIn: datasource, |
| useUnuseAction: 'use', |
| }), |
| }, |
| { |
| icon: IconNames.IMPORT, |
| title: 'Mark as unused segments by interval', |
| |
| onAction: () => |
| this.setState({ |
| datasourceToMarkSegmentsByIntervalIn: datasource, |
| useUnuseAction: 'unuse', |
| }), |
| }, |
| { |
| icon: IconNames.IMPORT, |
| title: 'Mark as unused all segments', |
| intent: Intent.DANGER, |
| onAction: () => this.setState({ datasourceToMarkAsUnusedAllSegmentsIn: datasource }), |
| }, |
| { |
| icon: IconNames.TRASH, |
| title: 'Delete unused segments (issue kill task)', |
| intent: Intent.DANGER, |
| onAction: () => this.setState({ killDatasource: datasource }), |
| }, |
| ]); |
| } |
| } |
| |
| renderRetentionDialog(): JSX.Element | undefined { |
| const { retentionDialogOpenOn, tiersState, datasourcesAndDefaultRulesState } = this.state; |
| const { defaultRules } = datasourcesAndDefaultRulesState.data || { |
| datasources: [], |
| defaultRules: [], |
| }; |
| if (!retentionDialogOpenOn) return; |
| |
| return ( |
| <RetentionDialog |
| datasource={retentionDialogOpenOn.datasource} |
| rules={retentionDialogOpenOn.rules} |
| tiers={tiersState.data || []} |
| onEditDefaults={this.editDefaultRules} |
| defaultRules={defaultRules} |
| onCancel={() => this.setState({ retentionDialogOpenOn: undefined })} |
| onSave={this.saveRules} |
| /> |
| ); |
| } |
| |
| renderCompactionDialog() { |
| const { datasourcesAndDefaultRulesState, compactionDialogOpenOn } = this.state; |
| if (!compactionDialogOpenOn || !datasourcesAndDefaultRulesState.data) return; |
| |
| return ( |
| <CompactionDialog |
| datasource={compactionDialogOpenOn.datasource} |
| compactionConfig={compactionDialogOpenOn.compactionConfig} |
| onClose={() => this.setState({ compactionDialogOpenOn: undefined })} |
| onSave={this.saveCompaction} |
| onDelete={this.deleteCompaction} |
| /> |
| ); |
| } |
| |
| renderDatasourceTable() { |
| const { goToSegments, capabilities } = this.props; |
| const { |
| datasourcesAndDefaultRulesState, |
| datasourceFilter, |
| showUnused, |
| hiddenColumns, |
| } = this.state; |
| |
| let { datasources, defaultRules } = datasourcesAndDefaultRulesState.data |
| ? datasourcesAndDefaultRulesState.data |
| : { datasources: [], defaultRules: [] }; |
| |
| if (!showUnused) { |
| datasources = datasources.filter(d => !d.unused); |
| } |
| |
| // Calculate column values for bracing |
| |
| const totalDataSizeValues = datasources.map(d => formatTotalDataSize(d.total_data_size)); |
| |
| const minSegmentRowsValues = datasources.map(d => formatSegmentRows(d.min_segment_rows)); |
| |
| const avgSegmentRowsValues = datasources.map(d => formatSegmentRows(d.avg_segment_rows)); |
| |
| const maxSegmentRowsValues = datasources.map(d => formatSegmentRows(d.max_segment_rows)); |
| |
| const totalRowsValues = datasources.map(d => formatTotalRows(d.total_rows)); |
| |
| const avgRowSizeValues = datasources.map(d => formatAvgRowSize(d.avg_row_size)); |
| |
| const replicatedSizeValues = datasources.map(d => formatReplicatedSize(d.replicated_size)); |
| |
| const leftToBeCompactedValues = datasources.map(d => |
| d.compactionStatus |
| ? formatLeftToBeCompacted(d.compactionStatus.bytesAwaitingCompaction) |
| : '-', |
| ); |
| |
| return ( |
| <> |
| <ReactTable |
| data={datasources} |
| loading={datasourcesAndDefaultRulesState.loading} |
| noDataText={ |
| datasourcesAndDefaultRulesState.getErrorMessage() || |
| (!datasourcesAndDefaultRulesState.loading && datasources && !datasources.length |
| ? 'No datasources' |
| : '') |
| } |
| filterable |
| filtered={datasourceFilter} |
| onFilteredChange={filtered => { |
| this.setState({ datasourceFilter: filtered }); |
| }} |
| columns={[ |
| { |
| Header: twoLines('Datasource', 'name'), |
| show: hiddenColumns.exists('Datasource name'), |
| accessor: 'datasource', |
| width: 150, |
| Cell: ({ value }) => { |
| return ( |
| <a |
| onClick={() => { |
| this.setState({ |
| datasourceFilter: addFilter(datasourceFilter, 'datasource', value), |
| }); |
| }} |
| > |
| {value} |
| </a> |
| ); |
| }, |
| }, |
| { |
| Header: 'Availability', |
| show: hiddenColumns.exists('Availability'), |
| filterable: false, |
| minWidth: 200, |
| accessor: 'num_segments', |
| Cell: ({ value: num_segments, original }) => { |
| const { datasource, unused, num_segments_to_load } = original; |
| if (unused) { |
| return ( |
| <span> |
| <span style={{ color: DatasourcesView.UNUSED_COLOR }}>● </span> |
| Unused |
| </span> |
| ); |
| } |
| |
| const segmentsEl = ( |
| <a onClick={() => goToSegments(datasource)}> |
| {pluralIfNeeded(num_segments, 'segment')} |
| </a> |
| ); |
| if (typeof num_segments_to_load !== 'number' || typeof num_segments !== 'number') { |
| return '-'; |
| } else if (num_segments_to_load === 0) { |
| return ( |
| <span> |
| <span style={{ color: DatasourcesView.FULLY_AVAILABLE_COLOR }}> |
| ● |
| </span> |
| Fully available ({segmentsEl}) |
| </span> |
| ); |
| } else { |
| const numAvailableSegments = num_segments - num_segments_to_load; |
| const percentAvailable = ( |
| Math.floor((numAvailableSegments / num_segments) * 1000) / 10 |
| ).toFixed(1); |
| return ( |
| <span> |
| <span style={{ color: DatasourcesView.PARTIALLY_AVAILABLE_COLOR }}> |
| {numAvailableSegments ? '\u25cf' : '\u25cb'} |
| </span> |
| {percentAvailable}% available ({segmentsEl}) |
| </span> |
| ); |
| } |
| }, |
| sortMethod: (d1, d2) => { |
| const percentAvailable1 = d1.num_available / d1.num_total; |
| const percentAvailable2 = d2.num_available / d2.num_total; |
| return percentAvailable1 - percentAvailable2 || d1.num_total - d2.num_total; |
| }, |
| }, |
| { |
| Header: twoLines('Availability', 'detail'), |
| show: hiddenColumns.exists('Availability detail'), |
| accessor: 'num_segments_to_load', |
| filterable: false, |
| minWidth: 100, |
| Cell: ({ original }) => { |
| const { num_segments_to_load, num_segments_to_drop } = original; |
| return formatLoadDrop(num_segments_to_load, num_segments_to_drop); |
| }, |
| }, |
| { |
| Header: twoLines('Total', 'data size'), |
| show: hiddenColumns.exists('Total data size'), |
| accessor: 'total_data_size', |
| filterable: false, |
| width: 100, |
| Cell: ({ value }) => ( |
| <BracedText text={formatTotalDataSize(value)} braces={totalDataSizeValues} /> |
| ), |
| }, |
| { |
| Header: twoLines('Segment size (rows)', 'minimum / average / maximum'), |
| show: capabilities.hasSql() && hiddenColumns.exists('Segment size'), |
| accessor: 'avg_segment_rows', |
| filterable: false, |
| width: 220, |
| Cell: ({ value, original }) => { |
| const { min_segment_rows, max_segment_rows } = original; |
| if (isNaN(value) || isNaN(min_segment_rows) || isNaN(max_segment_rows)) return '-'; |
| return ( |
| <> |
| <BracedText |
| text={formatSegmentRows(min_segment_rows)} |
| braces={minSegmentRowsValues} |
| />{' '} |
| {' '} |
| <BracedText text={formatSegmentRows(value)} braces={avgSegmentRowsValues} />{' '} |
| {' '} |
| <BracedText |
| text={formatSegmentRows(max_segment_rows)} |
| braces={maxSegmentRowsValues} |
| /> |
| </> |
| ); |
| }, |
| }, |
| { |
| Header: twoLines('Segment', 'granularity'), |
| show: capabilities.hasSql() && hiddenColumns.exists('Segment granularity'), |
| id: 'segment_granularity', |
| accessor: segmentGranularityCountsToRank, |
| filterable: false, |
| width: 100, |
| Cell: ({ original }) => { |
| const { |
| num_segments, |
| minute_aligned_segments, |
| hour_aligned_segments, |
| day_aligned_segments, |
| month_aligned_segments, |
| year_aligned_segments, |
| } = original; |
| const segmentGranularities: string[] = []; |
| if (!num_segments || isNaN(year_aligned_segments)) return '-'; |
| if (num_segments - minute_aligned_segments) { |
| segmentGranularities.push('Sub minute'); |
| } |
| if (minute_aligned_segments - hour_aligned_segments) { |
| segmentGranularities.push('Minute'); |
| } |
| if (hour_aligned_segments - day_aligned_segments) { |
| segmentGranularities.push('Hour'); |
| } |
| if (day_aligned_segments - month_aligned_segments) { |
| segmentGranularities.push('Day'); |
| } |
| if (month_aligned_segments - year_aligned_segments) { |
| segmentGranularities.push('Month'); |
| } |
| if (year_aligned_segments) { |
| segmentGranularities.push('Year'); |
| } |
| return segmentGranularities.join(', '); |
| }, |
| }, |
| { |
| Header: twoLines('Total', 'rows'), |
| show: capabilities.hasSql() && hiddenColumns.exists('Total rows'), |
| accessor: 'total_rows', |
| filterable: false, |
| width: 100, |
| Cell: ({ value }) => { |
| if (isNaN(value)) return '-'; |
| return <BracedText text={formatTotalRows(value)} braces={totalRowsValues} />; |
| }, |
| }, |
| { |
| Header: twoLines('Avg. row size', '(bytes)'), |
| show: capabilities.hasSql() && hiddenColumns.exists('Avg. row size'), |
| accessor: 'avg_row_size', |
| filterable: false, |
| width: 100, |
| Cell: ({ value }) => { |
| if (isNaN(value)) return '-'; |
| return <BracedText text={formatAvgRowSize(value)} braces={avgRowSizeValues} />; |
| }, |
| }, |
| { |
| Header: twoLines('Replicated', 'size'), |
| show: capabilities.hasSql() && hiddenColumns.exists('Replicated size'), |
| accessor: 'replicated_size', |
| filterable: false, |
| width: 100, |
| Cell: ({ value }) => { |
| if (isNaN(value)) return '-'; |
| return ( |
| <BracedText text={formatReplicatedSize(value)} braces={replicatedSizeValues} /> |
| ); |
| }, |
| }, |
| { |
| Header: 'Compaction', |
| show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Compaction'), |
| id: 'compactionStatus', |
| accessor: row => Boolean(row.compactionStatus), |
| filterable: false, |
| width: 150, |
| Cell: ({ original }) => { |
| const { datasource, compactionConfig, compactionStatus } = original; |
| return ( |
| <span |
| className="clickable-cell" |
| onClick={() => |
| this.setState({ |
| compactionDialogOpenOn: { |
| datasource, |
| compactionConfig, |
| }, |
| }) |
| } |
| > |
| {formatCompactionConfigAndStatus(compactionConfig, compactionStatus)} |
| <ActionIcon icon={IconNames.EDIT} /> |
| </span> |
| ); |
| }, |
| }, |
| { |
| Header: twoLines('% Compacted', 'bytes / segments / intervals'), |
| show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('% Compacted'), |
| id: 'percentCompacted', |
| width: 200, |
| accessor: ({ compactionStatus }) => |
| compactionStatus && compactionStatus.bytesCompacted |
| ? compactionStatus.bytesCompacted / |
| (compactionStatus.bytesAwaitingCompaction + compactionStatus.bytesCompacted) |
| : 0, |
| filterable: false, |
| Cell: ({ original }) => { |
| const { compactionStatus } = original; |
| |
| if (!compactionStatus || zeroCompactionStatus(compactionStatus)) { |
| return ( |
| <> |
| <BracedText text="-" braces={PERCENT_BRACES} /> {' '} |
| <BracedText text="-" braces={PERCENT_BRACES} /> {' '} |
| <BracedText text="-" braces={PERCENT_BRACES} /> |
| </> |
| ); |
| } |
| |
| return ( |
| <> |
| <BracedText |
| text={formatPercent( |
| progress( |
| compactionStatus.bytesCompacted, |
| compactionStatus.bytesAwaitingCompaction, |
| ), |
| )} |
| braces={PERCENT_BRACES} |
| />{' '} |
| {' '} |
| <BracedText |
| text={formatPercent( |
| progress( |
| compactionStatus.segmentCountCompacted, |
| compactionStatus.segmentCountAwaitingCompaction, |
| ), |
| )} |
| braces={PERCENT_BRACES} |
| />{' '} |
| {' '} |
| <BracedText |
| text={formatPercent( |
| progress( |
| compactionStatus.intervalCountCompacted, |
| compactionStatus.intervalCountAwaitingCompaction, |
| ), |
| )} |
| braces={PERCENT_BRACES} |
| /> |
| </> |
| ); |
| }, |
| }, |
| { |
| Header: twoLines('Left to be', 'compacted'), |
| show: |
| capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Left to be compacted'), |
| id: 'leftToBeCompacted', |
| width: 100, |
| accessor: ({ compactionStatus }) => |
| (compactionStatus && compactionStatus.bytesAwaitingCompaction) || 0, |
| filterable: false, |
| Cell: ({ original }) => { |
| const { compactionStatus } = original; |
| |
| if (!compactionStatus) { |
| return <BracedText text="-" braces={leftToBeCompactedValues} />; |
| } |
| |
| return ( |
| <BracedText |
| text={formatLeftToBeCompacted(compactionStatus.bytesAwaitingCompaction)} |
| braces={leftToBeCompactedValues} |
| /> |
| ); |
| }, |
| }, |
| { |
| Header: 'Retention', |
| show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Retention'), |
| id: 'retention', |
| accessor: row => row.rules.length, |
| filterable: false, |
| minWidth: 100, |
| Cell: ({ original }) => { |
| const { datasource, rules } = original; |
| return ( |
| <span |
| onClick={() => |
| this.setState({ |
| retentionDialogOpenOn: { |
| datasource, |
| rules, |
| }, |
| }) |
| } |
| className="clickable-cell" |
| > |
| {rules.length |
| ? DatasourcesView.formatRules(rules) |
| : `Cluster default: ${DatasourcesView.formatRules(defaultRules)}`} |
| |
| <ActionIcon icon={IconNames.EDIT} /> |
| </span> |
| ); |
| }, |
| }, |
| { |
| Header: ACTION_COLUMN_LABEL, |
| show: hiddenColumns.exists(ACTION_COLUMN_LABEL), |
| accessor: 'datasource', |
| id: ACTION_COLUMN_ID, |
| width: ACTION_COLUMN_WIDTH, |
| filterable: false, |
| Cell: ({ value: datasource, original }) => { |
| const { unused, rules, compactionConfig } = original; |
| const datasourceActions = this.getDatasourceActions( |
| datasource, |
| unused, |
| rules, |
| compactionConfig, |
| ); |
| return ( |
| <ActionCell |
| onDetail={() => { |
| this.setState({ |
| datasourceTableActionDialogId: datasource, |
| actions: datasourceActions, |
| }); |
| }} |
| actions={datasourceActions} |
| /> |
| ); |
| }, |
| }, |
| ]} |
| defaultPageSize={50} |
| /> |
| {this.renderUnuseAction()} |
| {this.renderUseAction()} |
| {this.renderUseUnuseActionByInterval()} |
| {this.renderKillAction()} |
| {this.renderRetentionDialog()} |
| {this.renderCompactionDialog()} |
| {this.renderForceCompactAction()} |
| </> |
| ); |
| } |
| |
| render(): JSX.Element { |
| const { capabilities } = this.props; |
| const { |
| showUnused, |
| hiddenColumns, |
| showSegmentTimeline, |
| datasourceTableActionDialogId, |
| actions, |
| } = this.state; |
| |
| return ( |
| <div |
| className={classNames('datasource-view app-view', { |
| 'show-segment-timeline': showSegmentTimeline, |
| })} |
| > |
| <ViewControlBar label="Datasources"> |
| <RefreshButton |
| onRefresh={auto => { |
| this.refresh(auto); |
| }} |
| localStorageKey={LocalStorageKeys.DATASOURCES_REFRESH_RATE} |
| /> |
| {this.renderBulkDatasourceActions()} |
| <Switch |
| checked={showUnused} |
| label="Show unused" |
| onChange={() => this.toggleUnused(showUnused)} |
| disabled={!capabilities.hasCoordinatorAccess()} |
| /> |
| <Switch |
| checked={showSegmentTimeline} |
| label="Show segment timeline" |
| onChange={() => this.setState({ showSegmentTimeline: !showSegmentTimeline })} |
| disabled={!capabilities.hasSqlOrCoordinatorAccess()} |
| /> |
| <TableColumnSelector |
| columns={tableColumns[capabilities.getMode()]} |
| onChange={column => |
| this.setState(prevState => ({ |
| hiddenColumns: prevState.hiddenColumns.toggle(column), |
| })) |
| } |
| onClose={added => { |
| if (!added) return; |
| this.fetchDatasourceData(); |
| }} |
| tableColumnsHidden={hiddenColumns.storedArray} |
| /> |
| </ViewControlBar> |
| {showSegmentTimeline && <SegmentTimeline capabilities={capabilities} />} |
| {this.renderDatasourceTable()} |
| {datasourceTableActionDialogId && ( |
| <DatasourceTableActionDialog |
| datasourceId={datasourceTableActionDialogId} |
| actions={actions} |
| onClose={() => this.setState({ datasourceTableActionDialogId: undefined })} |
| /> |
| )} |
| </div> |
| ); |
| } |
| } |