Web-console: add Supervisor statistics table and show json query manager (#8176)

* add statistics table

* change error message

* add query managers

* update snapshots

* fixes

* add stricter types

* fix error type

* prettier fic

* fix undefined

* add typing

* fixe query manager
This commit is contained in:
mcbrewster 2019-07-30 12:53:46 -07:00 committed by Fangjin Yang
parent 0f8c902e94
commit 4e60afc86e
11 changed files with 360 additions and 174 deletions

View File

@ -1,5 +1,4 @@
{
"extends": "awesome-code-style/sasslint.json",
"rules": {
}
"rules": {}
}

View File

@ -50,41 +50,6 @@ exports[`rule editor matches snapshot 1`] = `
</div>
<div
class="main-area"
>
<div
class="loader"
>
<div
class="loader-logo"
>
<svg
viewBox="0 0 100 100"
>
<path
class="one"
d="M54.2,69.8h-2.7c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h2.7c11.5,0,23.8-7.4,23.8-23.7
c0-9.1-6.9-15.8-16.4-15.8H38c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h23.6c5.3,0,10.1,1.9,13.6,5.3c3.5,3.4,5.4,8,5.4,13.1
c0,6.6-2.3,13-6.3,17.7C69.5,66.8,62.5,69.8,54.2,69.8z"
/>
<path
class="two"
d="M55.7,59.5h-26c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h26c7.5,0,11.5-5.8,11.5-11.5
c0-4.2-3.2-7.3-7.7-7.3h-26c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h26c5.9,0,10.3,4.3,10.3,9.9c0,3.7-1.3,7.2-3.7,9.8
C63.5,58,59.9,59.5,55.7,59.5z"
/>
<path
class="three"
d="M27.2,38h-6.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h6.3c0.7,0,1.3,0.6,1.3,1.3S27.9,38,27.2,38z"
/>
<path
class="four"
d="M45.1,69.8h-5.8c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h5.8c0.7,0,1.3,0.6,1.3,1.3
C46.4,69.2,45.8,69.8,45.1,69.8z"
/>
</svg>
</div>
</div>
</div>
/>
</div>
`;

View File

@ -23,7 +23,7 @@ import React from 'react';
import { AppToaster } from '../../singletons/toaster';
import { UrlBaser } from '../../singletons/url-baser';
import { downloadFile } from '../../utils';
import { downloadFile, QueryManager } from '../../utils';
import { Loader } from '../loader/loader';
import './show-json.scss';
@ -35,39 +35,44 @@ export interface ShowJsonProps {
}
export interface ShowJsonState {
jsonValue: string;
jsonValue?: string;
loading: boolean;
error?: string;
}
export class ShowJson extends React.PureComponent<ShowJsonProps, ShowJsonState> {
private showJsonQueryManager: QueryManager<null, string>;
constructor(props: ShowJsonProps, context: any) {
super(props, context);
this.state = {
jsonValue: '',
loading: false,
};
this.getJsonInfo();
this.showJsonQueryManager = new QueryManager({
processQuery: async () => {
const { endpoint, transform } = this.props;
const resp = await axios.get(endpoint);
let data = resp.data;
if (transform) data = transform(data);
return typeof data === 'string' ? data : JSON.stringify(data, undefined, 2);
},
onStateChange: ({ result, loading, error }) => {
this.setState({
loading,
error,
jsonValue: result,
});
},
});
}
private getJsonInfo = async (): Promise<void> => {
const { endpoint, transform } = this.props;
try {
const resp = await axios.get(endpoint);
let data = resp.data;
if (transform) data = transform(data);
this.setState({
jsonValue: typeof data === 'string' ? data : JSON.stringify(data, undefined, 2),
});
} catch (e) {
this.setState({
jsonValue: `Error: ` + e.response.data,
});
}
};
componentDidMount(): void {
this.showJsonQueryManager.runQuery(null);
}
render(): JSX.Element {
const { endpoint, downloadFilename } = this.props;
const { jsonValue } = this.state;
const { jsonValue, error, loading } = this.state;
return (
<div className="show-json">
@ -75,18 +80,18 @@ export class ShowJson extends React.PureComponent<ShowJsonProps, ShowJsonState>
<ButtonGroup className="right-buttons">
{downloadFilename && (
<Button
disabled={!jsonValue}
disabled={loading}
text="Save"
minimal
onClick={() => downloadFile(jsonValue, 'json', downloadFilename)}
onClick={() => downloadFile(jsonValue ? jsonValue : '', 'json', downloadFilename)}
/>
)}
<Button
text="Copy"
disabled={!jsonValue}
minimal
disabled={loading}
onClick={() => {
copy(jsonValue, { format: 'text/plain' });
copy(jsonValue ? jsonValue : '', { format: 'text/plain' });
AppToaster.show({
message: 'JSON copied to clipboard',
intent: Intent.SUCCESS,
@ -102,8 +107,7 @@ export class ShowJson extends React.PureComponent<ShowJsonProps, ShowJsonState>
</ButtonGroup>
</div>
<div className="main-area">
{!jsonValue && <Loader loadingText="" loading />}
{jsonValue && <TextArea readOnly value={jsonValue} />}
{loading ? <Loader /> : <TextArea readOnly value={!error ? jsonValue : error} />}
</div>
</div>
);

View File

@ -0,0 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule editor matches snapshot 1`] = `
<div
class="supervisor-statistics-table"
>
<div
class="top-actions"
>
<div
class="bp3-button-group right-buttons"
>
<button
class="bp3-button bp3-disabled bp3-minimal"
disabled=""
tabindex="-1"
type="button"
>
<span
class="bp3-button-text"
>
View raw
</span>
</button>
</div>
</div>
<div
class="main-area"
>
<div
class="loader"
>
<div
class="loader-logo"
>
<svg
viewBox="0 0 100 100"
>
<path
class="one"
d="M54.2,69.8h-2.7c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h2.7c11.5,0,23.8-7.4,23.8-23.7
c0-9.1-6.9-15.8-16.4-15.8H38c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h23.6c5.3,0,10.1,1.9,13.6,5.3c3.5,3.4,5.4,8,5.4,13.1
c0,6.6-2.3,13-6.3,17.7C69.5,66.8,62.5,69.8,54.2,69.8z"
/>
<path
class="two"
d="M55.7,59.5h-26c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h26c7.5,0,11.5-5.8,11.5-11.5
c0-4.2-3.2-7.3-7.7-7.3h-26c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h26c5.9,0,10.3,4.3,10.3,9.9c0,3.7-1.3,7.2-3.7,9.8
C63.5,58,59.9,59.5,55.7,59.5z"
/>
<path
class="three"
d="M27.2,38h-6.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h6.3c0.7,0,1.3,0.6,1.3,1.3S27.9,38,27.2,38z"
/>
<path
class="four"
d="M45.1,69.8h-5.8c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h5.8c0.7,0,1.3,0.6,1.3,1.3
C46.4,69.2,45.8,69.8,45.1,69.8z"
/>
</svg>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,49 @@
/*
* 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.
*/
.supervisor-statistics-table {
position: relative;
height: 100%;
.top-actions {
text-align: right;
padding-bottom: 10px;
& > * {
display: inline-block;
}
}
.main-area {
height: calc(100% - 40px);
textarea {
height: 100%;
width: 100%;
resize: none;
}
.loader {
position: relative;
}
.ReactTable {
height: 100%;
}
}
}

View File

@ -0,0 +1,30 @@
/*
* 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 { render } from '@testing-library/react';
import React from 'react';
import { SupervisorStatisticsTable } from './supervisor-statistics-table';
describe('rule editor', () => {
it('matches snapshot', () => {
const showJson = <SupervisorStatisticsTable endpoint={'test'} downloadFilename={'test'} />;
const { container } = render(showJson);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -0,0 +1,178 @@
/*
* 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 { Button, ButtonGroup } from '@blueprintjs/core';
import axios from 'axios';
import React from 'react';
import ReactTable, { Column } from 'react-table';
import { Loader } from '..';
import { UrlBaser } from '../../singletons/url-baser';
import { QueryManager } from '../../utils';
import { deepGet } from '../../utils/object-change';
import './supervisor-statistics-table.scss';
interface TaskSummary {
totals: Record<string, StatsEntry>;
movingAverages: Record<string, Record<string, StatsEntry>>;
}
interface StatsEntry {
processed?: number;
processedWithError?: number;
thrownAway?: number;
unparseable?: number;
[key: string]: number | undefined;
}
interface TableRow {
taskId: string;
summary: TaskSummary;
}
export interface SupervisorStatisticsTableProps {
endpoint: string;
downloadFilename?: string;
}
export interface SupervisorStatisticsTableState {
data?: TableRow[];
loading: boolean;
error?: string;
}
export class SupervisorStatisticsTable extends React.PureComponent<
SupervisorStatisticsTableProps,
SupervisorStatisticsTableState
> {
private supervisorStatisticsTableQueryManager: QueryManager<null, TableRow[]>;
constructor(props: SupervisorStatisticsTableProps, context: any) {
super(props, context);
this.state = {
loading: true,
};
this.supervisorStatisticsTableQueryManager = new QueryManager({
processQuery: async () => {
const { endpoint } = this.props;
const resp = await axios.get(endpoint);
const data: Record<string, Record<string, TaskSummary>> = resp.data;
return Object.values(data).flatMap(v =>
Object.keys(v).map(k => ({ taskId: k, summary: v[k] })),
);
},
onStateChange: ({ result, loading, error }) => {
this.setState({
data: result,
error,
loading,
});
},
});
}
componentDidMount(): void {
this.supervisorStatisticsTableQueryManager.runQuery(null);
}
renderCell(data: StatsEntry | undefined) {
if (!data) {
return <div>No data found</div>;
}
return Object.keys(data).map(key => (
<div key={key}>{`${key}: ${Number(data[key]).toFixed(1)}`}</div>
));
}
renderTable(error?: string) {
const { data } = this.state;
console.log(data);
let columns: Column<TableRow>[] = [
{
Header: 'Task Id',
id: 'task_id',
accessor: d => d.taskId,
},
{
Header: 'Totals',
id: 'total',
accessor: d => {
return deepGet(d, 'summary.totals.buildSegments') as StatsEntry;
},
Cell: d => {
return this.renderCell(d.value ? d.value : undefined);
},
},
];
if (data && data.length) {
columns = columns.concat(
Object.keys(deepGet(data[0], 'summary.movingAverages.buildSegments'))
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
.map(
(interval: string): Column<TableRow> => {
return {
Header: interval,
id: interval,
accessor: d => {
return deepGet(d, `summary.movingAverages.buildSegments.${interval}`);
},
Cell: d => {
return this.renderCell(d.value ? d.value : null);
},
};
},
),
);
}
return (
<ReactTable
data={this.state.data ? this.state.data : []}
showPagination={false}
defaultPageSize={6}
columns={columns}
noDataText={error ? error : 'No statistics data found'}
/>
);
}
render() {
const { endpoint } = this.props;
const { loading, error } = this.state;
return (
<div className="supervisor-statistics-table">
<div className="top-actions">
<ButtonGroup className="right-buttons">
<Button
text="View raw"
disabled={loading}
minimal
onClick={() => window.open(UrlBaser.base(endpoint), '_blank')}
/>
</ButtonGroup>
</div>
<div className="main-area">
{loading ? <Loader loadingText="" loading /> : !loading && this.renderTable(error)}
</div>
</div>
);
}
}

View File

@ -140,42 +140,7 @@ exports[`task table action dialog matches snapshot 1`] = `
</div>
<div
class="main-area"
>
<div
class="loader"
>
<div
class="loader-logo"
>
<svg
viewBox="0 0 100 100"
>
<path
class="one"
d="M54.2,69.8h-2.7c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h2.7c11.5,0,23.8-7.4,23.8-23.7
c0-9.1-6.9-15.8-16.4-15.8H38c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h23.6c5.3,0,10.1,1.9,13.6,5.3c3.5,3.4,5.4,8,5.4,13.1
c0,6.6-2.3,13-6.3,17.7C69.5,66.8,62.5,69.8,54.2,69.8z"
/>
<path
class="two"
d="M55.7,59.5h-26c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h26c7.5,0,11.5-5.8,11.5-11.5
c0-4.2-3.2-7.3-7.7-7.3h-26c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h26c5.9,0,10.3,4.3,10.3,9.9c0,3.7-1.3,7.2-3.7,9.8
C63.5,58,59.9,59.5,55.7,59.5z"
/>
<path
class="three"
d="M27.2,38h-6.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h6.3c0.7,0,1.3,0.6,1.3,1.3S27.9,38,27.2,38z"
/>
<path
class="four"
d="M45.1,69.8h-5.8c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h5.8c0.7,0,1.3,0.6,1.3,1.3
C46.4,69.2,45.8,69.8,45.1,69.8z"
/>
</svg>
</div>
</div>
</div>
/>
</div>
</div>
</div>

View File

@ -227,42 +227,7 @@ exports[`supervisor table action dialog matches snapshot 1`] = `
</div>
<div
class="main-area"
>
<div
class="loader"
>
<div
class="loader-logo"
>
<svg
viewBox="0 0 100 100"
>
<path
class="one"
d="M54.2,69.8h-2.7c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h2.7c11.5,0,23.8-7.4,23.8-23.7
c0-9.1-6.9-15.8-16.4-15.8H38c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h23.6c5.3,0,10.1,1.9,13.6,5.3c3.5,3.4,5.4,8,5.4,13.1
c0,6.6-2.3,13-6.3,17.7C69.5,66.8,62.5,69.8,54.2,69.8z"
/>
<path
class="two"
d="M55.7,59.5h-26c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h26c7.5,0,11.5-5.8,11.5-11.5
c0-4.2-3.2-7.3-7.7-7.3h-26c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h26c5.9,0,10.3,4.3,10.3,9.9c0,3.7-1.3,7.2-3.7,9.8
C63.5,58,59.9,59.5,55.7,59.5z"
/>
<path
class="three"
d="M27.2,38h-6.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h6.3c0.7,0,1.3,0.6,1.3,1.3S27.9,38,27.2,38z"
/>
<path
class="four"
d="M45.1,69.8h-5.8c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h5.8c0.7,0,1.3,0.6,1.3,1.3
C46.4,69.2,45.8,69.8,45.1,69.8z"
/>
</svg>
</div>
</div>
</div>
/>
</div>
</div>
</div>

View File

@ -21,6 +21,7 @@ import React 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 { BasicAction, basicActionsToButtons } from '../../utils/basic-action';
import { deepGet } from '../../utils/object-change';
import { SideButtonMetaData, TableActionDialog } from '../table-action-dialog/table-action-dialog';
@ -92,7 +93,7 @@ export class SupervisorTableActionDialog extends React.PureComponent<
/>
)}
{activeTab === 'stats' && (
<ShowJson
<SupervisorStatisticsTable
endpoint={`/druid/indexer/v1/supervisor/${supervisorId}/stats`}
downloadFilename={`supervisor-stats-${supervisorId}.json`}
/>

View File

@ -227,42 +227,7 @@ exports[`task table action dialog matches snapshot 1`] = `
</div>
<div
class="main-area"
>
<div
class="loader"
>
<div
class="loader-logo"
>
<svg
viewBox="0 0 100 100"
>
<path
class="one"
d="M54.2,69.8h-2.7c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h2.7c11.5,0,23.8-7.4,23.8-23.7
c0-9.1-6.9-15.8-16.4-15.8H38c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h23.6c5.3,0,10.1,1.9,13.6,5.3c3.5,3.4,5.4,8,5.4,13.1
c0,6.6-2.3,13-6.3,17.7C69.5,66.8,62.5,69.8,54.2,69.8z"
/>
<path
class="two"
d="M55.7,59.5h-26c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h26c7.5,0,11.5-5.8,11.5-11.5
c0-4.2-3.2-7.3-7.7-7.3h-26c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h26c5.9,0,10.3,4.3,10.3,9.9c0,3.7-1.3,7.2-3.7,9.8
C63.5,58,59.9,59.5,55.7,59.5z"
/>
<path
class="three"
d="M27.2,38h-6.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h6.3c0.7,0,1.3,0.6,1.3,1.3S27.9,38,27.2,38z"
/>
<path
class="four"
d="M45.1,69.8h-5.8c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h5.8c0.7,0,1.3,0.6,1.3,1.3
C46.4,69.2,45.8,69.8,45.1,69.8z"
/>
</svg>
</div>
</div>
</div>
/>
</div>
</div>
</div>