This is an automated email from the ASF dual-hosted git repository.
vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new e3f7217 Web console: Improve the handling of extreme data (funky
datasources, longs) (#10641)
e3f7217 is described below
commit e3f721754691f0f9f946f59c74c1dc41f5164173
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Tue Dec 8 09:25:14 2020 -0800
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
---
licenses.yaml | 12 ++-
licenses/bin/json-bigint-native.MIT | 20 +++++
web-console/README.md | 89 +++++++++++++---------
web-console/package-lock.json | 11 ++-
web-console/package.json | 4 +-
web-console/src/components/auto-form/auto-form.tsx | 5 ++
.../segment-timeline/segment-timeline.tsx | 4 +-
.../supervisor-statistics-table.tsx | 2 +-
.../dialogs/retention-dialog/retention-dialog.tsx | 2 +-
.../segment-table-action-dialog.tsx | 9 ++-
.../supervisor-table-action-dialog.tsx | 8 +-
.../task-table-action-dialog.tsx | 10 ++-
.../src/druid-models/ingestion-spec.spec.ts | 8 ++
web-console/src/druid-models/ingestion-spec.tsx | 7 ++
web-console/src/druid-models/time.ts | 2 +-
web-console/src/entry.ts | 5 +-
web-console/src/singletons/{api.ts => api.spec.ts} | 15 ++--
web-console/src/singletons/api.ts | 24 ++++++
.../src/views/datasource-view/datasource-view.tsx | 46 +++++++----
.../src/views/ingestion-view/ingestion-view.tsx | 19 +++--
.../src/views/load-data-view/load-data-view.tsx | 23 +++++-
.../src/views/lookups-view/lookups-view.tsx | 6 +-
.../src/views/segments-view/segments-view.tsx | 11 ++-
.../src/views/services-view/services-view.tsx | 4 +-
24 files changed, 245 insertions(+), 101 deletions(-)
diff --git a/licenses.yaml b/licenses.yaml
index e14f54a..90a7ffd 100644
--- a/licenses.yaml
+++ b/licenses.yaml
@@ -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
diff --git a/licenses/bin/json-bigint-native.MIT
b/licenses/bin/json-bigint-native.MIT
new file mode 100644
index 0000000..9776c81
--- /dev/null
+++ b/licenses/bin/json-bigint-native.MIT
@@ -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.
diff --git a/web-console/README.md b/web-console/README.md
index 1283589..cd57370 100644
--- a/web-console/README.md
+++ b/web-console/README.md
@@ -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,40 +38,31 @@ To try the console in (say) coordinator mode you could run
it as such:
`druid_host=localhost:8081 npm start`
-## Description of the directory structure
+### Developing
-A lot of the directory structure was created to preserve the existing console
structure as much as possible.
+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.
-As part of this repo:
+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.
-- `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
-- `script/` - Some helper bash scripts for running this console
-- `src/` - This directory (together with `lib`) constitutes all the source
code for this console
+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.
-## List of non SQL data reading APIs used
+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:
-```
-GET /status
-GET /druid/indexer/v1/supervisor?full
-POST /druid/indexer/v1/worker
-GET /druid/indexer/v1/workers
-GET /druid/indexer/v1/tasks
-GET /druid/coordinator/v1/loadqueue?simple
-GET /druid/coordinator/v1/config
-GET /druid/coordinator/v1/metadata/datasources?includeUnused
-GET /druid/coordinator/v1/rules
-GET /druid/coordinator/v1/config/compaction
-GET /druid/coordinator/v1/tiers
-```
+- `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
+### Updating the list of license files
-From the web-console directory run `script/licenses`
+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.
-## Running End-to-End Tests
+Please be conscious of not introducing dependencies on packages with Apache
incompatible licenses.
+
+### Running end-to-end tests
From the web-console directory:
@@ -81,21 +74,47 @@ From the web-console directory:
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
+#### 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
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
+#### 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`.
\ No newline at end of file
+development mode of the web console (started via `npm start`), which runs on
port `18081`.
+
+
+## Description of the directory structure
+
+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 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
+
+## List of non SQL data reading APIs used
+
+```
+GET /status
+GET /druid/indexer/v1/supervisor?full
+POST /druid/indexer/v1/worker
+GET /druid/indexer/v1/workers
+GET /druid/indexer/v1/tasks
+GET /druid/coordinator/v1/loadqueue?simple
+GET /druid/coordinator/v1/config
+GET /druid/coordinator/v1/metadata/datasources?includeUnused
+GET /druid/coordinator/v1/rules
+GET /druid/coordinator/v1/config/compaction
+GET /druid/coordinator/v1/tiers
+```
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index fccd993..26654d2 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -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",
diff --git a/web-console/package.json b/web-console/package.json
index c5c29f8..31548fd 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -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",
diff --git a/web-console/src/components/auto-form/auto-form.tsx
b/web-console/src/components/auto-form/auto-form.tsx
index 3b2f6c0..7b25db9 100644
--- a/web-console/src/components/auto-form/auto-form.tsx
+++ b/web-console/src/components/auto-form/auto-form.tsx
@@ -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') {
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx
b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 4ccddcb..5942a50 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -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)
diff --git
a/web-console/src/components/supervisor-statistics-table/supervisor-statistics-table.tsx
b/web-console/src/components/supervisor-statistics-table/supervisor-statistics-table.tsx
index f7514a6..bc9e240 100644
---
a/web-console/src/components/supervisor-statistics-table/supervisor-statistics-table.tsx
+++
b/web-console/src/components/supervisor-statistics-table/supervisor-statistics-table.tsx
@@ -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 () => {
diff --git a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
index c11d345..41efe0a 100644
--- a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
+++ b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
@@ -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;
},
diff --git
a/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx
b/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx
index 7271236..a33c3c3 100644
---
a/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx
+++
b/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx
@@ -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`}
/>
)}
diff --git
a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx
b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx
index e1c6b07..8e9fb4d 100644
---
a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx
+++
b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx
@@ -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`}
/>
)}
diff --git
a/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.tsx
b/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.tsx
index d030821..ecd8454 100644
---
a/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.tsx
+++
b/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.tsx
@@ -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}
/>
diff --git a/web-console/src/druid-models/ingestion-spec.spec.ts
b/web-console/src/druid-models/ingestion-spec.spec.ts
index fb6df06..e698128 100644
--- a/web-console/src/druid-models/ingestion-spec.spec.ts
+++ b/web-console/src/druid-models/ingestion-spec.spec.ts
@@ -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');
+ });
});
diff --git a/web-console/src/druid-models/ingestion-spec.tsx
b/web-console/src/druid-models/ingestion-spec.tsx
index baa8b50..73963da 100644
--- a/web-console/src/druid-models/ingestion-spec.tsx
+++ b/web-console/src/druid-models/ingestion-spec.tsx
@@ -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
+}
diff --git a/web-console/src/druid-models/time.ts
b/web-console/src/druid-models/time.ts
index c20d2cb..352b230 100644
--- a/web-console/src/druid-models/time.ts
+++ b/web-console/src/druid-models/time.ts
@@ -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':
diff --git a/web-console/src/entry.ts b/web-console/src/entry.ts
index 45fe093..45a6d5e 100644
--- a/web-console/src/entry.ts
+++ b/web-console/src/entry.ts
@@ -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;
diff --git a/web-console/src/singletons/api.ts
b/web-console/src/singletons/api.spec.ts
similarity index 77%
copy from web-console/src/singletons/api.ts
copy to web-console/src/singletons/api.spec.ts
index 1e8f537..64429af 100644
--- a/web-console/src/singletons/api.ts
+++ b/web-console/src/singletons/api.spec.ts
@@ -16,12 +16,11 @@
* limitations under the License.
*/
-import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
+import { Api } from './api';
-export class Api {
- static instance: AxiosInstance;
-
- static initialize(config?: AxiosRequestConfig): void {
- Api.instance = axios.create(config);
- }
-}
+describe('Api', () => {
+ it('escapes stuff', () => {
+ expect(Api.encodePath('wikipedia')).toEqual('wikipedia');
+ expect(Api.encodePath('wi%ki?pe#dia')).toEqual('wi%25ki%3Fpe%23dia');
+ });
+});
diff --git a/web-console/src/singletons/api.ts
b/web-console/src/singletons/api.ts
index 1e8f537..a05adf3 100644
--- a/web-console/src/singletons/api.ts
+++ b/web-console/src/singletons/api.ts
@@ -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);
+ }
}
diff --git a/web-console/src/views/datasource-view/datasource-view.tsx
b/web-console/src/views/datasource-view/datasource-view.tsx
index 678f984..adecc91 100644
--- a/web-console/src/views/datasource-view/datasource-view.tsx
+++ b/web-console/src/views/datasource-view/datasource-view.tsx
@@ -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;
diff --git a/web-console/src/views/ingestion-view/ingestion-view.tsx
b/web-console/src/views/ingestion-view/ingestion-view.tsx
index 4dd15df..bb6106f 100644
--- a/web-console/src/views/ingestion-view/ingestion-view.tsx
+++ b/web-console/src/views/ingestion-view/ingestion-view.tsx
@@ -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"
diff --git a/web-console/src/views/load-data-view/load-data-view.tsx
b/web-console/src/views/load-data-view/load-data-view.tsx
index 75b427a..edf29b6 100644
--- a/web-console/src/views/load-data-view/load-data-view.tsx
+++ b/web-console/src/views/load-data-view/load-data-view.tsx
@@ -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 (
diff --git a/web-console/src/views/lookups-view/lookups-view.tsx
b/web-console/src/views/lookups-view/lookups-view.tsx
index 566af56..6de19ec 100644
--- a/web-console/src/views/lookups-view/lookups-view.tsx
+++ b/web-console/src/views/lookups-view/lookups-view.tsx
@@ -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"
diff --git a/web-console/src/views/segments-view/segments-view.tsx
b/web-console/src/views/segments-view/segments-view.tsx
index 08568b8..c3c4abb 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -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}
diff --git a/web-console/src/views/services-view/services-view.tsx
b/web-console/src/views/services-view/services-view.tsx
index 38d58d6..a51d9f2 100644
--- a/web-console/src/views/services-view/services-view.tsx
+++ b/web-console/src/views/services-view/services-view.tsx
@@ -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;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]