mirror of https://github.com/apache/druid.git
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:
parent
9acab0b646
commit
e3f7217546
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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`.
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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`}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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`}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue