Web console: Improve the handling of extreme data (funky datasources, longs) (#10641)

* better API escape

* fix escaping issue, bigints

* update licenses

* fix align

* do not show Query with SQL if no SQL

* add prettify script

* update dev readme

* add ordering to the datasource list

* add ordering to supervisor table
This commit is contained in:
Vadim Ogievetsky 2020-12-08 09:25:14 -08:00 committed by GitHub
parent 9acab0b646
commit e3f7217546
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 271 additions and 100 deletions

View File

@ -4751,7 +4751,7 @@ license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: Imply Data
version: 0.10.4
version: 0.10.5
---
@ -4915,6 +4915,16 @@ license_file_path: licenses/bin/js-tokens.MIT
---
name: "json-bigint-native"
license_category: binary
module: web-console
license_name: MIT License
copyright: Vadim Ogievetsky, Andrey Sidorov
version: 1.0.0
license_file_path: licenses/bin/json-bigint-native.MIT
---
name: "lodash.debounce"
license_category: binary
module: web-console

View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2020 Vadim Ogievetsky, Andrey Sidorov
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -19,13 +19,15 @@
# Apache Druid web console
This is the unified Druid web console that servers as a data management layer for Druid.
This is the Druid web console that servers as a data management interface for Druid.
## How to watch and run for development
## Developing the console
### Getting started
1. You need to be within the `web-console` directory
2. Install the modules with `npm install`
3. Run `npm run compile` to compile the scss files
3. Run `npm run compile` to compile the scss files (this usually needs to be done only once)
4. Run `npm start` will start in development mode and will proxy druid requests to `localhost:8888`
@ -36,16 +38,68 @@ To try the console in (say) coordinator mode you could run it as such:
`druid_host=localhost:8081 npm start`
### Developing
You should use a TypeScript friendly IDE (such as [WebStorm](https://www.jetbrains.com/webstorm/), or [VS Code](https://code.visualstudio.com/)) to develop the web console.
The console relies on [tslint](https://palantir.github.io/tslint/), [sass-lint](https://github.com/sasstools/sass-lint), and [prettier](https://prettier.io/) to enforce the code style.
If you are going to do any non-trivial development you should set up file watchers in your IDE to automatically fix your code as you type.
If you do not set up auto file watchers then even a trivial change such as a typo fix might draw the ire of the code style enforcement (it might require some lines to be re-wrapped).
If you find yourself in that position you should run on or more of:
- `npm run tslint-fix`
- `npm run sasslint-fix`
- `npm run prettify`
To get your code into an acceptable state.
### Updating the list of license files
If you change the dependencies of the console in any way please run `script/licenses` (from the web-console directory).
It will analyze the changes and update the `../licenses` file as needed.
Please be conscious of not introducing dependencies on packages with Apache incompatible licenses.
### Running end-to-end tests
From the web-console directory:
1. Build druid distribution: `script/druid build`
2. Start druid cluster: `script/druid start`
3. Run end-to-end tests: `npm run test-e2e`
4. Stop druid cluster: `script/druid stop`
If you already have a druid cluster running on the standard ports, the steps to build/start/stop a druid cluster can
be skipped.
#### Screenshots for debugging
`e2e-tests/util/debug.ts:saveScreenshotIfError()` is used to save a screenshot of the web console
when the test fails. For example, if `e2e-tests/tutorial-batch.spec.ts` fails, it will create
`load-data-from-local-disk-error-screenshot.png`.
#### Disabling headless mode
Disabling headless mode while running the tests can be helpful. This can be done via the `DRUID_E2E_TEST_HEADLESS`
environment variable, which defaults to `true`.
#### Running against alternate web console
The environment variable `DRUID_E2E_TEST_UNIFIED_CONSOLE_PORT` can be used to target a web console running on a
non-default port (i.e., not port `8888`). For example, this environment variable can be used to target the
development mode of the web console (started via `npm start`), which runs on port `18081`.
## Description of the directory structure
A lot of the directory structure was created to preserve the existing console structure as much as possible.
As part of this repo:
As part of this directory:
- `assets/` - The images (and other assets) used within the console
- `e2e-tests/` - End-to-end tests for the console
- `lib/` - A place where some overrides to the react-table stylus files live, this is outside of the normal SCSS build system.
- `public/` - The compiled destination of the file powering this console
- `public/` - The compiled destination for the files powering this console
- `script/` - Some helper bash scripts for running this console
- `src/` - This directory (together with `lib`) constitutes all the source code for this console
@ -64,38 +118,3 @@ GET /druid/coordinator/v1/rules
GET /druid/coordinator/v1/config/compaction
GET /druid/coordinator/v1/tiers
```
## Updating the list of license files
From the web-console directory run `script/licenses`
## Running End-to-End Tests
From the web-console directory:
1. Build druid distribution: `script/druid build`
2. Start druid cluster: `script/druid start`
3. Run end-to-end tests: `npm run test-e2e`
4. Stop druid cluster: `script/druid stop`
If you already have a druid cluster running on the standard ports, the steps to build/start/stop a druid cluster can
be skipped.
### Debugging
#### Screenshots
`e2e-tests/util/debug.ts:saveScreenshotIfError()` is used to save a screenshot of the web console
when the test fails. For example, if `e2e-tests/tutorial-batch.spec.ts` fails, it will create
`load-data-from-local-disk-error-screenshot.png`.
#### Disabling Headless Mode
Disabling headless mode while running the tests can be helpful. This can be done via the `DRUID_E2E_TEST_HEADLESS`
environment variable, which defaults to `true`.
#### Running Against Alternate Web Console
The environment variable `DRUID_E2E_TEST_UNIFIED_CONSOLE_PORT` can be used to target a web console running on a
non-default port (i.e., not port `8888`). For example, this environment variable can be used to target the
development mode of the web console (started via `npm start`), which runs on port `18081`.

View File

@ -4265,9 +4265,9 @@
}
},
"druid-query-toolkit": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.10.4.tgz",
"integrity": "sha512-feIRTC2paOkGpWvymseMs/wn+8XfbLjlcBsXJXKxgsJtqMKBYy3f8YiN3SV/xv6CQP9Vv4nBMEoa5q8OM5KHsg==",
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.10.5.tgz",
"integrity": "sha512-qdH1FsjxAgGnXHtk9F88j3XT+/KLYfuPcVCMxBBolYE1/O1O6in5FDW+id8ek0JT/+astNMGKjfh6IUk9s/YkQ==",
"requires": {
"tslib": "^2.0.2"
},
@ -8037,6 +8037,11 @@
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true
},
"json-bigint-native": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint-native/-/json-bigint-native-1.0.0.tgz",
"integrity": "sha512-upUMnqV96WRGAbopHwDFHxsJbdBZRAdZW3WEJTB/WMLUseDEhJkcOQ0qr5x+JOHwIWpI9BYc6zoVJkmbL7SMLA=="
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",

View File

@ -51,6 +51,7 @@
"sasslint-fix": "npm run sasslint -- --fix",
"sasslint-changed-only": "git diff --diff-filter=ACMR --name-only | grep -E \\.scss$ | xargs ./node_modules/.bin/stylelint --config sasslint.json",
"sasslint-fix-changed-only": "npm run sasslint-changed-only -- --fix",
"prettify": "prettier --write '{src,e2e-tests}/**/*.{ts,tsx,scss}'",
"generate-licenses-file": "license-checker --production --json --out licenses.json",
"check-licenses": "license-checker --production --onlyAllow 'Apache-1.1;Apache-2.0;BSD-2-Clause;BSD-3-Clause;0BSD;MIT;CC0-1.0' --summary",
"start": "webpack-dev-server --hot --open"
@ -68,11 +69,12 @@
"d3-axis": "^1.0.12",
"d3-scale": "^3.2.0",
"d3-selection": "^1.4.0",
"druid-query-toolkit": "^0.10.4",
"druid-query-toolkit": "^0.10.5",
"file-saver": "^2.0.2",
"fontsource-open-sans": "^3.0.9",
"has-own-prop": "^2.0.0",
"hjson": "^3.2.1",
"json-bigint-native": "^1.0.0",
"lodash.debounce": "^4.0.8",
"lodash.escape": "^4.0.1",
"memoize-one": "^5.1.1",

View File

@ -56,6 +56,7 @@ export interface Field<M> {
defined?: Functor<M, boolean>;
required?: Functor<M, boolean>;
hideInMore?: Functor<M, boolean>;
valueAdjustment?: (value: any) => any;
adjustment?: (model: M) => M;
issueWithValue?: (value: any) => string | undefined;
}
@ -156,6 +157,10 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
const { model } = this.props;
if (!model) return;
if (field.valueAdjustment) {
newValue = field.valueAdjustment(newValue);
}
let newModel: T;
if (typeof newValue === 'undefined') {
if (typeof field.emptyValue === 'undefined') {

View File

@ -270,7 +270,9 @@ ORDER BY "start" DESC`;
intervals = (await Promise.all(
datasources.map(async datasource => {
const intervalMap = (await Api.instance.get(
`/druid/coordinator/v1/datasources/${datasource}/intervals?simple`,
`/druid/coordinator/v1/datasources/${Api.encodePath(
datasource,
)}/intervals?simple`,
)).data;
return Object.keys(intervalMap)

View File

@ -60,7 +60,7 @@ export const SupervisorStatisticsTable = React.memo(function SupervisorStatistic
props: SupervisorStatisticsTableProps,
) {
const { supervisorId } = props;
const endpoint = `/druid/indexer/v1/supervisor/${supervisorId}/stats`;
const endpoint = `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/stats`;
const [supervisorStatisticsState] = useQueryManager<null, SupervisorStatisticsTableRow[]>({
processQuery: async () => {

View File

@ -47,7 +47,7 @@ export const RetentionDialog = React.memo(function RetentionDialog(props: Retent
const [historyQueryState] = useQueryManager<string, any[]>({
processQuery: async datasource => {
const historyResp = await Api.instance.get(
`/druid/coordinator/v1/rules/${datasource}/history`,
`/druid/coordinator/v1/rules/${Api.encodePath(datasource)}/history`,
);
return historyResp.data;
},

View File

@ -19,12 +19,13 @@
import React, { useState } from 'react';
import { ShowJson } from '../../components';
import { Api } from '../../singletons';
import { BasicAction } from '../../utils/basic-action';
import { SideButtonMetaData, TableActionDialog } from '../table-action-dialog/table-action-dialog';
interface SegmentTableActionDialogProps {
segmentId?: string;
datasourceId?: string;
segmentId: string;
datasourceId: string;
actions: BasicAction[];
onClose: () => void;
}
@ -53,7 +54,9 @@ export const SegmentTableActionDialog = React.memo(function SegmentTableActionDi
>
{activeTab === 'metadata' && (
<ShowJson
endpoint={`/druid/coordinator/v1/metadata/datasources/${datasourceId}/segments/${segmentId}`}
endpoint={`/druid/coordinator/v1/metadata/datasources/${Api.encodePath(
datasourceId,
)}/segments/${Api.encodePath(segmentId)}`}
downloadFilename={`Segment-metadata-${segmentId}.json`}
/>
)}

View File

@ -21,6 +21,7 @@ import React, { useState } from 'react';
import { ShowJson } from '../../components';
import { ShowHistory } from '../../components/show-history/show-history';
import { SupervisorStatisticsTable } from '../../components/supervisor-statistics-table/supervisor-statistics-table';
import { Api } from '../../singletons';
import { deepGet } from '../../utils';
import { BasicAction } from '../../utils/basic-action';
import { SideButtonMetaData, TableActionDialog } from '../table-action-dialog/table-action-dialog';
@ -64,6 +65,7 @@ export const SupervisorTableActionDialog = React.memo(function SupervisorTableAc
},
];
const supervisorEndpointBase = `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}`;
return (
<TableActionDialog
sideButtonMetadata={supervisorTableSideButtonMetadata}
@ -73,7 +75,7 @@ export const SupervisorTableActionDialog = React.memo(function SupervisorTableAc
>
{activeTab === 'status' && (
<ShowJson
endpoint={`/druid/indexer/v1/supervisor/${supervisorId}/status`}
endpoint={`${supervisorEndpointBase}/status`}
transform={x => deepGet(x, 'payload')}
downloadFilename={`supervisor-status-${supervisorId}.json`}
/>
@ -86,13 +88,13 @@ export const SupervisorTableActionDialog = React.memo(function SupervisorTableAc
)}
{activeTab === 'payload' && (
<ShowJson
endpoint={`/druid/indexer/v1/supervisor/${supervisorId}`}
endpoint={supervisorEndpointBase}
downloadFilename={`supervisor-payload-${supervisorId}.json`}
/>
)}
{activeTab === 'history' && (
<ShowHistory
endpoint={`/druid/indexer/v1/supervisor/${supervisorId}/history`}
endpoint={`${supervisorEndpointBase}/history`}
downloadFilename={`supervisor-history-${supervisorId}.json`}
/>
)}

View File

@ -19,6 +19,7 @@
import React, { useState } from 'react';
import { ShowJson, ShowLog } from '../../components';
import { Api } from '../../singletons';
import { deepGet } from '../../utils';
import { BasicAction } from '../../utils/basic-action';
import { SideButtonMetaData, TableActionDialog } from '../table-action-dialog/table-action-dialog';
@ -63,6 +64,7 @@ export const TaskTableActionDialog = React.memo(function TaskTableActionDialog(
},
];
const taskEndpointBase = `/druid/indexer/v1/task/${Api.encodePath(taskId)}`;
return (
<TableActionDialog
sideButtonMetadata={taskTableSideButtonMetadata}
@ -72,21 +74,21 @@ export const TaskTableActionDialog = React.memo(function TaskTableActionDialog(
>
{activeTab === 'status' && (
<ShowJson
endpoint={`/druid/indexer/v1/task/${taskId}/status`}
endpoint={`${taskEndpointBase}/status`}
transform={x => deepGet(x, 'status')}
downloadFilename={`task-status-${taskId}.json`}
/>
)}
{activeTab === 'payload' && (
<ShowJson
endpoint={`/druid/indexer/v1/task/${taskId}`}
endpoint={taskEndpointBase}
transform={x => deepGet(x, 'payload')}
downloadFilename={`task-payload-${taskId}.json`}
/>
)}
{activeTab === 'reports' && (
<ShowJson
endpoint={`/druid/indexer/v1/task/${taskId}/reports`}
endpoint={`${taskEndpointBase}/reports`}
transform={x => deepGet(x, 'ingestionStatsAndErrors.payload')}
downloadFilename={`task-reports-${taskId}.json`}
/>
@ -94,7 +96,7 @@ export const TaskTableActionDialog = React.memo(function TaskTableActionDialog(
{activeTab === 'log' && (
<ShowLog
status={status}
endpoint={`/druid/indexer/v1/task/${taskId}/log`}
endpoint={`${taskEndpointBase}/log`}
downloadFilename={`task-log-${taskId}.log`}
tailOffset={16000}
/>

View File

@ -17,6 +17,7 @@
*/
import {
adjustId,
cleanSpec,
downgradeSpec,
getColumnTypeFromHeaderAndRows,
@ -255,4 +256,11 @@ describe('spec utils', () => {
}
`);
});
it('adjustId', () => {
expect(adjustId('')).toEqual('');
expect(adjustId('lol')).toEqual('lol');
expect(adjustId('.l/o/l')).toEqual('lol');
expect(adjustId('l\t \nl')).toEqual('l l');
});
});

View File

@ -2211,3 +2211,10 @@ export function downgradeSpec(spec: any): any {
}
return spec;
}
export function adjustId(id: string): string {
return id
.replace(/\//g, '') // Can not have /
.replace(/^\./, '') // Can not have leading .
.replace(/\s+/gm, ' '); // Can not have whitespaces other than space
}

View File

@ -60,7 +60,7 @@ export const ISO_MATCHER = /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([1
// Note: AUTO and ISO are basically the same except ISO has a space as a separator instead of the T
export function timeFormatMatches(format: string, value: string | number): boolean {
export function timeFormatMatches(format: string, value: string | number | bigint): boolean {
const absValue = Math.abs(Number(value));
switch (format) {
case 'auto':

View File

@ -16,7 +16,6 @@
* limitations under the License.
*/
import { AxiosRequestConfig } from 'axios';
import 'core-js/stable';
import React from 'react';
import ReactDOM from 'react-dom';
@ -67,9 +66,7 @@ if (typeof consoleConfig.title === 'string') {
window.document.title = consoleConfig.title;
}
const apiConfig: AxiosRequestConfig = {
headers: {},
};
const apiConfig = Api.getDefaultConfig();
if (consoleConfig.baseURL) {
apiConfig.baseURL = consoleConfig.baseURL;

View File

@ -0,0 +1,26 @@
/*
* 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 { Api } from './api';
describe('Api', () => {
it('escapes stuff', () => {
expect(Api.encodePath('wikipedia')).toEqual('wikipedia');
expect(Api.encodePath('wi%ki?pe#dia')).toEqual('wi%25ki%3Fpe%23dia');
});
});

View File

@ -17,6 +17,7 @@
*/
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import * as JSONBig from 'json-bigint-native';
export class Api {
static instance: AxiosInstance;
@ -24,4 +25,27 @@ export class Api {
static initialize(config?: AxiosRequestConfig): void {
Api.instance = axios.create(config);
}
static getDefaultConfig(): AxiosRequestConfig {
return {
headers: {},
transformResponse: [
data => {
if (typeof data === 'string') {
try {
data = JSONBig.parse(data);
} catch (e) {
/* Ignore */
}
}
return data;
},
],
};
}
static encodePath(path: string): string {
return path.replace(/[?#%]/g, encodeURIComponent);
}
}

View File

@ -267,7 +267,8 @@ export class DatasourcesView extends React.PureComponent<
ELSE 0
END AS avg_row_size
FROM sys.segments
GROUP BY 1`;
GROUP BY 1
ORDER BY 1`;
static formatRules(rules: Rule[]): string {
if (rules.length === 0) {
@ -461,7 +462,9 @@ GROUP BY 1`;
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.delete(
`/druid/coordinator/v1/datasources/${datasourceToMarkAsUnusedAllSegmentsIn}`,
`/druid/coordinator/v1/datasources/${Api.encodePath(
datasourceToMarkAsUnusedAllSegmentsIn,
)}`,
{},
);
return resp.data;
@ -492,7 +495,9 @@ GROUP BY 1`;
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.post(
`/druid/coordinator/v1/datasources/${datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn}`,
`/druid/coordinator/v1/datasources/${Api.encodePath(
datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn,
)}`,
{},
);
return resp.data;
@ -524,7 +529,9 @@ GROUP BY 1`;
if (!useUnuseInterval) return;
const param = isUse ? 'markUsed' : 'markUnused';
const resp = await Api.instance.post(
`/druid/coordinator/v1/datasources/${datasourceToMarkSegmentsByIntervalIn}/${param}`,
`/druid/coordinator/v1/datasources/${Api.encodePath(
datasourceToMarkSegmentsByIntervalIn,
)}/${Api.encodePath(param)}`,
{
interval: useUnuseInterval,
},
@ -566,7 +573,9 @@ GROUP BY 1`;
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.delete(
`/druid/coordinator/v1/datasources/${killDatasource}?kill=true&interval=1000/3000`,
`/druid/coordinator/v1/datasources/${Api.encodePath(
killDatasource,
)}?kill=true&interval=1000/3000`,
{},
);
return resp.data;
@ -653,7 +662,7 @@ GROUP BY 1`;
private saveRules = async (datasource: string, rules: Rule[], comment: string) => {
try {
await Api.instance.post(`/druid/coordinator/v1/rules/${datasource}`, rules, {
await Api.instance.post(`/druid/coordinator/v1/rules/${Api.encodePath(datasource)}`, rules, {
headers: {
'X-Druid-Author': 'console',
'X-Druid-Comment': comment,
@ -716,7 +725,9 @@ GROUP BY 1`;
text: 'Confirm',
onClick: async () => {
try {
await Api.instance.delete(`/druid/coordinator/v1/config/compaction/${datasource}`);
await Api.instance.delete(
`/druid/coordinator/v1/config/compaction/${Api.encodePath(datasource)}`,
);
this.setState({ compactionDialogOpenOn: undefined }, () =>
this.datasourceQueryManager.rerunLastQuery(),
);
@ -746,18 +757,21 @@ GROUP BY 1`;
): BasicAction[] {
const { goToQuery, goToTask, capabilities } = this.props;
const goToActions: BasicAction[] = [
{
const goToActions: BasicAction[] = [];
if (capabilities.hasSql()) {
goToActions.push({
icon: IconNames.APPLICATION,
title: 'Query with SQL',
onAction: () => goToQuery(SqlQuery.create(SqlRef.table(datasource)).toString()),
},
{
icon: IconNames.GANTT_CHART,
title: 'Go to tasks',
onAction: () => goToTask(datasource),
},
];
});
}
goToActions.push({
icon: IconNames.GANTT_CHART,
title: 'Go to tasks',
onAction: () => goToTask(datasource),
});
if (!capabilities.hasCoordinatorAccess()) {
return goToActions;

View File

@ -193,8 +193,10 @@ export class IngestionView extends React.PureComponent<IngestionViewProps, Inges
FAILED: 1,
};
static SUPERVISOR_SQL = `SELECT "supervisor_id", "type", "source", "state", "detailed_state", "suspended"
FROM sys.supervisors`;
static SUPERVISOR_SQL = `SELECT
"supervisor_id", "type", "source", "state", "detailed_state", "suspended"
FROM sys.supervisors
ORDER BY "supervisor_id"`;
static TASK_SQL = `SELECT
"task_id", "group_id", "type", "datasource", "created_time", "location", "duration", "error_msg",
@ -431,7 +433,7 @@ ORDER BY "rank" DESC, "created_time" DESC`;
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.post(
`/druid/indexer/v1/supervisor/${resumeSupervisorId}/resume`,
`/druid/indexer/v1/supervisor/${Api.encodePath(resumeSupervisorId)}/resume`,
{},
);
return resp.data;
@ -460,7 +462,7 @@ ORDER BY "rank" DESC, "created_time" DESC`;
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.post(
`/druid/indexer/v1/supervisor/${suspendSupervisorId}/suspend`,
`/druid/indexer/v1/supervisor/${Api.encodePath(suspendSupervisorId)}/suspend`,
{},
);
return resp.data;
@ -489,7 +491,7 @@ ORDER BY "rank" DESC, "created_time" DESC`;
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.post(
`/druid/indexer/v1/supervisor/${resetSupervisorId}/reset`,
`/druid/indexer/v1/supervisor/${Api.encodePath(resetSupervisorId)}/reset`,
{},
);
return resp.data;
@ -527,7 +529,7 @@ ORDER BY "rank" DESC, "created_time" DESC`;
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.post(
`/druid/indexer/v1/supervisor/${terminateSupervisorId}/terminate`,
`/druid/indexer/v1/supervisor/${Api.encodePath(terminateSupervisorId)}/terminate`,
{},
);
return resp.data;
@ -683,7 +685,10 @@ ORDER BY "rank" DESC, "created_time" DESC`;
return (
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.post(`/druid/indexer/v1/task/${killTaskId}/shutdown`, {});
const resp = await Api.instance.post(
`/druid/indexer/v1/task/${Api.encodePath(killTaskId)}/shutdown`,
{},
);
return resp.data;
}}
confirmButtonText="Kill task"

View File

@ -56,6 +56,7 @@ import { FormGroupWithInfo } from '../../components/form-group-with-info/form-gr
import { AsyncActionDialog } from '../../dialogs';
import {
addTimestampTransform,
adjustId,
CONSTANT_TIMESTAMP_SPEC,
CONSTANT_TIMESTAMP_SPEC_FIELDS,
DIMENSION_SPEC_FIELDS,
@ -3130,7 +3131,16 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
name: 'spec.dataSchema.dataSource',
label: 'Datasource name',
type: 'string',
info: <>This is the name of the datasource (table) in Druid.</>,
valueAdjustment: d => (typeof d === 'string' ? adjustId(d) : d),
info: (
<>
<p>This is the name of the datasource (table) in Druid.</p>
<p>
The datasource name can not start with a dot <Code>.</Code>, include slashes{' '}
<Code>/</Code>, or have whitespace other than space.
</p>
</>
),
},
{
name: 'spec.ioConfig.appendToExisting',
@ -3216,9 +3226,12 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
// ==================================================================
private getSupervisorJson = async (): Promise<void> => {
const { initSupervisorId } = this.props;
if (!initSupervisorId) return;
try {
const resp = await Api.instance.get(`/druid/indexer/v1/supervisor/${initSupervisorId}`);
const resp = await Api.instance.get(
`/druid/indexer/v1/supervisor/${Api.encodePath(initSupervisorId)}`,
);
this.updateSpec(cleanSpec(resp.data));
this.setState({ continueToSpec: true });
this.updateStep('spec');
@ -3232,9 +3245,10 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
private getTaskJson = async (): Promise<void> => {
const { initTaskId } = this.props;
if (!initTaskId) return;
try {
const resp = await Api.instance.get(`/druid/indexer/v1/task/${initTaskId}`);
const resp = await Api.instance.get(`/druid/indexer/v1/task/${Api.encodePath(initTaskId)}`);
this.updateSpec(cleanSpec(resp.data.payload));
this.setState({ continueToSpec: true });
this.updateStep('spec');
@ -3252,7 +3266,8 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
const fullSpec = Boolean(
deepGet(spec, 'spec.dataSchema.timestampSpec') &&
deepGet(spec, 'spec.dataSchema.dimensionsSpec') &&
deepGet(spec, 'spec.dataSchema.granularitySpec.type'),
deepGet(spec, 'spec.dataSchema.granularitySpec.type') &&
deepGet(spec, 'spec.dataSchema.dataSource'),
);
return (

View File

@ -261,13 +261,15 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
renderDeleteLookupAction() {
const { deleteLookupTier, deleteLookupName } = this.state;
if (!deleteLookupTier) return;
if (!deleteLookupTier || !deleteLookupName) return;
return (
<AsyncActionDialog
action={async () => {
await Api.instance.delete(
`/druid/coordinator/v1/lookups/config/${deleteLookupTier}/${deleteLookupName}`,
`/druid/coordinator/v1/lookups/config/${Api.encodePath(
deleteLookupTier,
)}/${Api.encodePath(deleteLookupName)}`,
);
}}
confirmButtonText="Delete lookup"

View File

@ -300,8 +300,9 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
)).data;
const nestedResults: SegmentQueryResultRow[][] = await Promise.all(
datasourceList.map(async (d: string) => {
const segments = (await Api.instance.get(`/druid/coordinator/v1/datasources/${d}?full`))
.data.segments;
const segments = (await Api.instance.get(
`/druid/coordinator/v1/datasources/${Api.encodePath(d)}?full`,
)).data.segments;
return segments.map(
(segment: any): SegmentQueryResultRow => {
@ -637,7 +638,9 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.delete(
`/druid/coordinator/v1/datasources/${terminateDatasourceId}/segments/${terminateSegmentId}`,
`/druid/coordinator/v1/datasources/${Api.encodePath(
terminateDatasourceId,
)}/segments/${Api.encodePath(terminateSegmentId)}`,
{},
);
return resp.data;
@ -742,7 +745,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
{this.renderSegmentsTable()}
</div>
{this.renderTerminateSegmentAction()}
{segmentTableActionDialogId && (
{segmentTableActionDialogId && datasourceTableActionDialogId && (
<SegmentTableActionDialog
segmentId={segmentTableActionDialogId}
datasourceId={datasourceTableActionDialogId}

View File

@ -592,7 +592,7 @@ ORDER BY "rank" DESC, "service" DESC`;
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.post(
`/druid/indexer/v1/worker/${middleManagerDisableWorkerHost}/disable`,
`/druid/indexer/v1/worker/${Api.encodePath(middleManagerDisableWorkerHost)}/disable`,
{},
);
return resp.data;
@ -621,7 +621,7 @@ ORDER BY "rank" DESC, "service" DESC`;
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.post(
`/druid/indexer/v1/worker/${middleManagerEnableWorkerHost}/enable`,
`/druid/indexer/v1/worker/${Api.encodePath(middleManagerEnableWorkerHost)}/enable`,
{},
);
return resp.data;