This is an automated email from the ASF dual-hosted git repository.
fjy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-druid.git
The following commit(s) were added to refs/heads/master by this push:
new 01f3da6 Web Console: add Group by interval to segments view (#7986)
01f3da6 is described below
commit 01f3da6fda150280c074337a38880d2113786df0
Author: mcbrewster <[email protected]>
AuthorDate: Fri Jul 19 11:25:25 2019 -0700
Web Console: add Group by interval to segments view (#7986)
* add group by
* update snapshot
* fix spacing
* change design pattern
* rename interface
* add static function
* save
* add whereClause"
* add default page size
* add where to inner query
* snapshots
---
web-console/script/mkcomp | 26 ++-
web-console/src/utils/query-manager.tsx | 16 +-
.../__snapshots__/segments-view.spec.tsx.snap | 28 ++-
.../src/views/segments-view/segments-view.tsx | 232 ++++++++++++++++-----
web-console/tsconfig.json | 8 +-
web-console/unified-console.html | 24 +--
web-console/webpack.config.js | 54 ++---
7 files changed, 276 insertions(+), 112 deletions(-)
diff --git a/web-console/script/mkcomp b/web-console/script/mkcomp
index 5634959..80bf859 100755
--- a/web-console/script/mkcomp
+++ b/web-console/script/mkcomp
@@ -44,14 +44,14 @@ fs.ensureDirSync(path);
console.log('Making path:', path);
const spaceName = name.replace(/-/g, ' ');
-const camelName = name.replace(/(^|-)[a-z]/g, (s) => s.replace('-',
'').toUpperCase());
+const camelName = name.replace(/(^|-)[a-z]/g, s => s.replace('-',
'').toUpperCase());
const snakeName = camelName[0].toLowerCase() + camelName.substr(1);
function writeFile(path, data) {
try {
return fs.writeFileSync(path, data, {
flag: 'wx', // x = fail if file exists
- encoding: 'utf8'
+ encoding: 'utf8',
});
} catch (error) {
return console.log(`Skipping ${path}`);
@@ -59,8 +59,9 @@ function writeFile(path, data) {
}
// Make the TypeScript file
-writeFile(path + name + '.tsx',
-`/*
+writeFile(
+ path + name + '.tsx',
+ `/*
* 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
@@ -104,11 +105,13 @@ export class ${camelName} extends
React.PureComponent<${camelName}Props, ${camel
</div>;
}
}
-`);
+`,
+);
// Make the SASS file
-writeFile(path + name + '.scss',
-`/*
+writeFile(
+ path + name + '.scss',
+ `/*
* 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
@@ -129,10 +132,12 @@ writeFile(path + name + '.scss',
.${name} {
}
-`);
+`,
+);
// Make the spec test file
-writeFile(path + name + '.spec.tsx',
+writeFile(
+ path + name + '.spec.tsx',
`/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -165,4 +170,5 @@ describe('${spaceName}', () => {
expect(container.firstChild).toMatchSnapshot();
});
});
-`);
+`,
+);
diff --git a/web-console/src/utils/query-manager.tsx
b/web-console/src/utils/query-manager.tsx
index 1317c7b..ecf78f1 100644
--- a/web-console/src/utils/query-manager.tsx
+++ b/web-console/src/utils/query-manager.tsx
@@ -25,19 +25,23 @@ export interface QueryStateInt<R> {
}
export interface QueryManagerOptions<Q, R> {
- processQuery: (query: Q) => Promise<R>;
+ processQuery: (query: Q, setIntermediateQuery: (intermediateQuery: any) =>
void) => Promise<R>;
onStateChange?: (queryResolve: QueryStateInt<R>) => void;
debounceIdle?: number;
debounceLoading?: number;
}
export class QueryManager<Q, R> {
- private processQuery: (query: Q) => Promise<R>;
+ private processQuery: (
+ query: Q,
+ setIntermediateQuery: (intermediateQuery: any) => void,
+ ) => Promise<R>;
private onStateChange?: (queryResolve: QueryStateInt<R>) => void;
private terminated = false;
private nextQuery: Q | undefined;
private lastQuery: Q | undefined;
+ private lastIntermediateQuery: any;
private actuallyLoading = false;
private state: QueryStateInt<R> = {
result: null,
@@ -78,7 +82,9 @@ export class QueryManager<Q, R> {
const myQueryId = this.currentQueryId;
this.actuallyLoading = true;
- this.processQuery(this.lastQuery).then(
+ this.processQuery(this.lastQuery, (intermediateQuery: any) => {
+ this.lastIntermediateQuery = intermediateQuery;
+ }).then(
result => {
if (this.currentQueryId !== myQueryId) return;
this.actuallyLoading = false;
@@ -136,6 +142,10 @@ export class QueryManager<Q, R> {
return this.lastQuery;
}
+ public getLastIntermediateQuery(): any {
+ return this.lastIntermediateQuery;
+ }
+
public getState(): QueryStateInt<R> {
return this.state;
}
diff --git
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
index 5179ad4..70e4003 100755
---
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
+++
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
@@ -18,6 +18,23 @@ exports[`segments-view matches snapshot 1`] = `
onClick={[Function]}
text="Go to SQL"
/>
+ <Component>
+ Group by
+ </Component>
+ <Blueprint3.ButtonGroup>
+ <Blueprint3.Button
+ active={true}
+ onClick={[Function]}
+ >
+ None
+ </Blueprint3.Button>
+ <Blueprint3.Button
+ active={false}
+ onClick={[Function]}
+ >
+ Interval
+ </Blueprint3.Button>
+ </Blueprint3.ButtonGroup>
<TableColumnSelector
columns={
Array [
@@ -112,6 +129,14 @@ exports[`segments-view matches snapshot 1`] = `
},
Object {
"Cell": [Function],
+ "Header": "Interval",
+ "accessor": "interval",
+ "defaultSortDesc": true,
+ "show": false,
+ "width": 120,
+ },
+ Object {
+ "Cell": [Function],
"Header": "Start",
"accessor": "start",
"defaultSortDesc": true,
@@ -209,7 +234,7 @@ exports[`segments-view matches snapshot 1`] = `
defaultFilterMethod={[Function]}
defaultFiltered={Array []}
defaultPage={0}
- defaultPageSize={50}
+ defaultPageSize={25}
defaultResized={Array []}
defaultSortDesc={false}
defaultSortMethod={[Function]}
@@ -287,6 +312,7 @@ exports[`segments-view matches snapshot 1`] = `
}
pageText="Page"
pages={10000000}
+ pivotBy={Array []}
pivotDefaults={Object {}}
pivotIDKey="_pivotID"
pivotValKey="_pivotVal"
diff --git a/web-console/src/views/segments-view/segments-view.tsx
b/web-console/src/views/segments-view/segments-view.tsx
index 2a65636..33b5c2b 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { Button, Intent } from '@blueprintjs/core';
+import { Button, ButtonGroup, Intent, Label } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import React from 'react';
@@ -86,11 +86,27 @@ export interface SegmentsViewState {
terminateSegmentId: string | null;
terminateDatasourceId: string | null;
hiddenColumns: LocalStorageBackedArray<string>;
+ loaded: boolean;
+ groupByInterval: boolean;
+
+ // table state
+ page: number | null;
+ pageSize: number | null;
+ filtered: number[] | null;
+ sorted: Sorted | null;
+}
+
+interface Sorted {
+ id: number;
+ desc: boolean;
}
-interface QueryAndSkip {
- query: string;
- skip: number;
+interface SegmentsQuery {
+ page: number;
+ pageSize: number;
+ filtered: Filter[];
+ sorted: Sorted[];
+ groupByInterval: boolean;
}
interface SegmentQueryResultRow {
@@ -111,7 +127,9 @@ interface SegmentQueryResultRow {
}
export class SegmentsView extends React.PureComponent<SegmentsViewProps,
SegmentsViewState> {
- private segmentsSqlQueryManager: QueryManager<QueryAndSkip,
SegmentQueryResultRow[]>;
+ static PAGE_SIZE = 25;
+
+ private segmentsSqlQueryManager: QueryManager<SegmentsQuery,
SegmentQueryResultRow[]>;
private segmentsNoSqlQueryManager: QueryManager<null,
SegmentQueryResultRow[]>;
constructor(props: SegmentsViewProps, context: any) {
@@ -134,11 +152,87 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
hiddenColumns: new LocalStorageBackedArray<string>(
LocalStorageKeys.SEGMENT_TABLE_COLUMN_SELECTION,
),
+ loaded: false,
+ groupByInterval: false,
+
+ // Table state
+ page: null,
+ pageSize: null,
+ sorted: null,
+ filtered: null,
};
this.segmentsSqlQueryManager = new QueryManager({
- processQuery: async (query: QueryAndSkip) => {
- const results: any[] = (await queryDruidSql({ query: query.query
})).slice(query.skip);
+ processQuery: async (query: SegmentsQuery, setIntermediateQuery) => {
+ const totalQuerySize = (query.page + 1) * query.pageSize;
+
+ const whereParts = query.filtered
+ .map((f: Filter) => {
+ if (f.id.startsWith('is_')) {
+ if (f.value === 'all') return null;
+ return `${JSON.stringify(f.id)} = ${f.value === 'true' ? 1 : 0}`;
+ } else {
+ return sqlQueryCustomTableFilter(f);
+ }
+ })
+ .filter(Boolean);
+
+ let queryParts: string[];
+ if (query.groupByInterval) {
+ queryParts = [
+ `SELECT`,
+ ` ("start" || '/' || "end") AS "interval",`,
+ ` "segment_id", "datasource", "start", "end", "size", "version",
"partition_num", "num_replicas", "num_rows", "is_published", "is_available",
"is_realtime", "is_overshadowed", "payload"`,
+ `FROM sys.segments`,
+ `WHERE`,
+ ];
+ if (whereParts.length) {
+ queryParts.push(whereParts.join(' AND ') + 'AND');
+ }
+ queryParts.push(
+ ` ("start" || '/' || "end") IN (SELECT "start" || '/' || "end"
FROM sys.segments GROUP BY 1 LIMIT ${totalQuerySize})`,
+ );
+
+ if (whereParts.length) {
+ queryParts.push('AND ' + whereParts.join(' AND '));
+ }
+
+ if (query.sorted.length) {
+ queryParts.push(
+ 'ORDER BY ' +
+ query.sorted
+ .map((sort: any) => `${JSON.stringify(sort.id)} ${sort.desc
? 'DESC' : 'ASC'}`)
+ .join(', '),
+ );
+ }
+
+ queryParts.push(`LIMIT ${totalQuerySize * 1000}`);
+ } else {
+ queryParts = [
+ `SELECT "segment_id", "datasource", "start", "end", "size",
"version", "partition_num", "num_replicas", "num_rows", "is_published",
"is_available", "is_realtime", "is_overshadowed", "payload"`,
+ `FROM sys.segments`,
+ ];
+
+ if (whereParts.length) {
+ queryParts.push('WHERE ' + whereParts.join(' AND '));
+ }
+
+ if (query.sorted.length) {
+ queryParts.push(
+ 'ORDER BY ' +
+ query.sorted
+ .map((sort: any) => `${JSON.stringify(sort.id)} ${sort.desc
? 'DESC' : 'ASC'}`)
+ .join(', '),
+ );
+ }
+
+ queryParts.push(`LIMIT ${totalQuerySize}`);
+ }
+ const sqlQuery = queryParts.join('\n');
+ setIntermediateQuery(sqlQuery);
+ const results: any[] = (await queryDruidSql({ query: sqlQuery
})).slice(
+ query.page * query.pageSize,
+ );
results.forEach(result => {
try {
result.payload = JSON.parse(result.payload);
@@ -194,7 +288,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
onStateChange: ({ result, loading, error }) => {
this.setState({
allSegments: result,
- segments: result ? result.slice(0, 50) : null,
+ segments: result ? result.slice(0, SegmentsView.PAGE_SIZE) : null,
segmentsLoading: loading,
segmentsError: error,
});
@@ -213,50 +307,19 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
this.segmentsNoSqlQueryManager.terminate();
}
- private fetchData = (state: any) => {
- const { page, pageSize, filtered, sorted } = state;
- const totalQuerySize = (page + 1) * pageSize;
-
- const queryParts = [
- `SELECT "segment_id", "datasource", "start", "end", "size", "version",
"partition_num", "num_replicas", "num_rows", "is_published", "is_available",
"is_realtime", "is_overshadowed", "payload"`,
- `FROM sys.segments`,
- ];
-
- const whereParts = filtered
- .map((f: Filter) => {
- if (f.id.startsWith('is_')) {
- if (f.value === 'all') return null;
- return `${JSON.stringify(f.id)} = ${f.value === 'true' ? 1 : 0}`;
- } else {
- return sqlQueryCustomTableFilter(f);
- }
- })
- .filter(Boolean);
-
- if (whereParts.length) {
- queryParts.push('WHERE ' + whereParts.join(' AND '));
- }
-
- if (sorted.length) {
- queryParts.push(
- 'ORDER BY ' +
- sorted
- .map((sort: any) => `${JSON.stringify(sort.id)} ${sort.desc ?
'DESC' : 'ASC'}`)
- .join(', '),
- );
- }
-
- queryParts.push(`LIMIT ${totalQuerySize}`);
-
- const query = queryParts.join('\n');
+ private fetchData = (groupByInterval: boolean, state?: any) => {
+ const { page, pageSize, filtered, sorted } = state ? state : this.state;
this.segmentsSqlQueryManager.runQuery({
- query,
- skip: totalQuerySize - pageSize,
+ page,
+ pageSize,
+ filtered,
+ sorted,
+ groupByInterval: groupByInterval,
});
};
- private fetchClientSideData = (state: any) => {
- const { page, pageSize, filtered, sorted } = state;
+ private fetchClientSideData = (state?: any) => {
+ const { page, pageSize, filtered, sorted } = state ? state : state;
const { allSegments } = this.state;
if (allSegments == null) return;
const startPage = page * pageSize;
@@ -296,7 +359,14 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
}
renderSegmentsTable() {
- const { segments, segmentsLoading, segmentsError, segmentFilter,
hiddenColumns } = this.state;
+ const {
+ segments,
+ segmentsLoading,
+ segmentsError,
+ segmentFilter,
+ hiddenColumns,
+ groupByInterval,
+ } = this.state;
const { noSqlMode } = this.props;
return (
@@ -314,9 +384,24 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
onFilteredChange={filtered => {
this.setState({ segmentFilter: filtered });
}}
- onFetchData={noSqlMode ? this.fetchClientSideData : this.fetchData}
+ onFetchData={
+ noSqlMode
+ ? this.fetchClientSideData
+ : state => {
+ this.setState({
+ page: state.page,
+ pageSize: state.pageSize,
+ filtered: state.filtered,
+ sorted: state.sorted,
+ });
+ if (this.segmentsSqlQueryManager.getLastQuery) {
+ this.fetchData(groupByInterval, state);
+ }
+ }
+ }
showPageJump={false}
ofText=""
+ pivotBy={groupByInterval ? ['interval'] : []}
columns={[
{
Header: 'Segment ID',
@@ -342,6 +427,25 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
show: hiddenColumns.exists('Datasource'),
},
{
+ Header: 'Interval',
+ accessor: 'interval',
+ width: 120,
+ defaultSortDesc: true,
+ Cell: row => {
+ const value = row.value;
+ return (
+ <a
+ onClick={() => {
+ this.setState({ segmentFilter: addFilter(segmentFilter,
'interval', value) });
+ }}
+ >
+ {value}
+ </a>
+ );
+ },
+ show: hiddenColumns.exists('interval') && groupByInterval,
+ },
+ {
Header: 'Start',
accessor: 'start',
width: 120,
@@ -472,7 +576,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
show: hiddenColumns.exists(ActionCell.COLUMN_LABEL),
},
]}
- defaultPageSize={50}
+ defaultPageSize={SegmentsView.PAGE_SIZE}
/>
);
}
@@ -516,7 +620,8 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
hiddenColumns,
} = this.state;
const { goToQuery, noSqlMode } = this.props;
- const lastSegmentsQuery = this.segmentsSqlQueryManager.getLastQuery();
+ const { groupByInterval } = this.state;
+ const lastSegmentsQuery =
this.segmentsSqlQueryManager.getLastIntermediateQuery();
return (
<>
@@ -537,10 +642,31 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
disabled={!lastSegmentsQuery}
onClick={() => {
if (!lastSegmentsQuery) return;
- goToQuery(lastSegmentsQuery.query);
+ goToQuery(lastSegmentsQuery);
}}
/>
)}
+ <Label>Group by</Label>
+ <ButtonGroup>
+ <Button
+ active={!groupByInterval}
+ onClick={() => {
+ this.setState({ groupByInterval: false });
+ noSqlMode ? this.fetchClientSideData() :
this.fetchData(false);
+ }}
+ >
+ None
+ </Button>
+ <Button
+ active={groupByInterval}
+ onClick={() => {
+ this.setState({ groupByInterval: true });
+ this.fetchData(true);
+ }}
+ >
+ Interval
+ </Button>
+ </ButtonGroup>
<TableColumnSelector
columns={noSqlMode ? tableColumnsNoSql : tableColumns}
onChange={column => this.setState({ hiddenColumns:
hiddenColumns.toggle(column) })}
diff --git a/web-console/tsconfig.json b/web-console/tsconfig.json
index 255116f..697bd76 100644
--- a/web-console/tsconfig.json
+++ b/web-console/tsconfig.json
@@ -18,13 +18,9 @@
"moduleResolution": "node",
"lib": ["dom", "es2016"],
"jsx": "react",
- "rootDirs": ["lib","src"],
+ "rootDirs": ["lib", "src"],
"outDir": "build"
},
- "include": [
- "src/**/*.ts",
- "src/**/*.tsx",
- "lib/sql-function-doc.ts"
- ]
+ "include": ["src/**/*.ts", "src/**/*.tsx", "lib/sql-function-doc.ts"]
}
diff --git a/web-console/unified-console.html b/web-console/unified-console.html
index 9f722af..81073f1 100644
--- a/web-console/unified-console.html
+++ b/web-console/unified-console.html
@@ -1,4 +1,4 @@
-<!doctype html>
+<!DOCTYPE html>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
@@ -18,15 +18,15 @@
~ under the License.
-->
<html lang="en">
-<head>
- <meta charset="utf-8">
- <title>Apache Druid</title>
- <meta name="description" content="Apache Druid console">
- <link rel="shortcut icon" href="favicon.png">
-</head>
-<body class="bp3-dark mouse-mode">
- <div class="app-container"></div>
- <script src="console-config.js"></script>
- <script src="public/web-console-0.16.0.js"></script>
-</body>
+ <head>
+ <meta charset="utf-8" />
+ <title>Apache Druid</title>
+ <meta name="description" content="Apache Druid console" />
+ <link rel="shortcut icon" href="favicon.png" />
+ </head>
+ <body class="bp3-dark mouse-mode">
+ <div class="app-container"></div>
+ <script src="console-config.js"></script>
+ <script src="public/web-console-0.16.0.js"></script>
+ </body>
</html>
diff --git a/web-console/webpack.config.js b/web-console/webpack.config.js
index d155384..cbb75a6 100644
--- a/web-console/webpack.config.js
+++ b/web-console/webpack.config.js
@@ -29,14 +29,14 @@ function friendlyErrorFormatter(e) {
return `${e.severity}: ${e.content} [TS${e.code}]\n at
(${e.file}:${e.line}:${e.character})`;
}
-module.exports = (env) => {
- let druidUrl = ((env || {}).druid_host || process.env.druid_host ||
'localhost');
+module.exports = env => {
+ let druidUrl = (env || {}).druid_host || process.env.druid_host ||
'localhost';
if (!druidUrl.startsWith('http')) druidUrl = 'http://' + druidUrl;
if (!/:\d+$/.test(druidUrl)) druidUrl += ':8888';
const proxyTarget = {
target: druidUrl,
- secure: false
+ secure: false,
};
const mode = process.env.NODE_ENV === 'production' ? 'production' :
'development';
@@ -45,17 +45,17 @@ module.exports = (env) => {
mode: mode,
devtool: 'hidden-source-map',
entry: {
- 'web-console': './src/entry.ts'
+ 'web-console': './src/entry.ts',
},
output: {
path: path.resolve(__dirname, './public'),
filename: `[name]-${version}.js`,
chunkFilename: `[name]-${version}.js`,
- publicPath: '/public'
+ publicPath: '/public',
},
target: 'web',
resolve: {
- extensions: ['.tsx', '.ts', '.html', '.js', '.json', '.scss', '.css']
+ extensions: ['.tsx', '.ts', '.html', '.js', '.json', '.scss', '.css'],
},
devServer: {
publicPath: '/public',
@@ -65,8 +65,8 @@ module.exports = (env) => {
proxy: {
'/status': proxyTarget,
'/druid': proxyTarget,
- '/proxy': proxyTarget
- }
+ '/proxy': proxyTarget,
+ },
},
module: {
rules: [
@@ -79,10 +79,10 @@ module.exports = (env) => {
options: {
configFile: 'tslint.json',
emitErrors: true,
- fix: false // Set this to true to auto fix errors
- }
- }
- ]
+ fix: false, // Set this to true to auto fix errors
+ },
+ },
+ ],
},
{
test: /\.tsx?$/,
@@ -91,10 +91,10 @@ module.exports = (env) => {
{
loader: 'ts-loader',
options: {
- errorFormatter: friendlyErrorFormatter
- }
- }
- ]
+ errorFormatter: friendlyErrorFormatter,
+ },
+ },
+ ],
},
{
test: (ALWAYS_BABEL || mode === 'production') ? /\.m?js$/ : /^xxx$/,
@@ -105,29 +105,29 @@ module.exports = (env) => {
{
test: /\.s?css$/,
use: [
- {loader: 'style-loader'}, // creates style nodes from JS strings
- {loader: 'css-loader'}, // translates CSS into CommonJS
+ { loader: 'style-loader' }, // creates style nodes from JS strings
+ { loader: 'css-loader' }, // translates CSS into CommonJS
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [
postcssPresetEnv({
- browsers: ['> 1%', 'last 3 versions', 'Firefox ESR',
'Opera 12.1']
- })
- ]
- }
+ browsers: ['> 1%', 'last 3 versions', 'Firefox ESR',
'Opera 12.1'],
+ }),
+ ],
+ },
},
- {loader: 'sass-loader'} // compiles Sass to CSS, using Node Sass
by default
- ]
- }
- ]
+ { loader: 'sass-loader' }, // compiles Sass to CSS, using Node
Sass by default
+ ],
+ },
+ ],
},
performance: {
hints: false
},
plugins: [
// new BundleAnalyzerPlugin()
- ]
+ ],
};
};
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]