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]

Reply via email to