Web console: refactor home view, add tests (#8247)

* refactor home view

* updated mode button placement
This commit is contained in:
Vadim Ogievetsky 2019-08-06 12:41:07 -07:00 committed by Fangjin Yang
parent 38b6047aa9
commit b9c68a5b7b
39 changed files with 1637 additions and 763 deletions

View File

@ -79,7 +79,6 @@ writeFile(
* limitations under the License.
*/
import { Button, InputGroup } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import React from 'react';
@ -156,8 +155,8 @@ writeFile(
* limitations under the License.
*/
import { render } from '@testing-library/react';
import React from 'react';
import { render } from 'react-testing-library';
import { ${camelName} } from './${name}';

View File

@ -1,15 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`jodaFormatToRegExp works for common formats 1`] = `"/^(?:3[0-1]|[12][0-9]|[1-9])\\\\/(?:1[0-2]|[1-9])\\\\/[0-9]{4}$/i"`;
exports[`jodaFormatToRegExp works for common formats 2`] = `"/^(?:1[0-2]|0[1-9])\\\\/(?:3[0-1]|[12][0-9]|0[1-9])\\\\/[0-9]{4}$/i"`;
exports[`jodaFormatToRegExp works for common formats 3`] = `"/^(?:1[0-2]|[1-9])\\\\/(?:3[0-1]|[12][0-9]|[1-9])\\\\/[0-9]{2}$/i"`;
exports[`jodaFormatToRegExp works for common formats 4`] = `"/^(?:3[0-1]|[12][0-9]|[1-9])-(?:1[0-2]|[1-9])-[0-9]{4} (?:1[0-2]|0[1-9]):[0-5][0-9]:[0-5][0-9] [ap]m$/i"`;
exports[`jodaFormatToRegExp works for common formats 5`] = `"/^(?:1[0-2]|0[1-9])\\\\/(?:3[0-1]|[12][0-9]|0[1-9])\\\\/[0-9]{4} (?:1[0-2]|0[1-9]):[0-5][0-9]:[0-5][0-9] [ap]m$/i"`;
exports[`jodaFormatToRegExp works for common formats 6`] = `"/^[0-9]{4}-(?:1[0-2]|0[1-9])-(?:3[0-1]|[12][0-9]|0[1-9]) (?:2[0-3]|1[0-9]|0[0-9]):[0-5][0-9]:[0-5][0-9]$/i"`;
exports[`jodaFormatToRegExp works for common formats 7`] = `"/^[0-9]{4}-(?:1[0-2]|0[1-9])-(?:3[0-1]|[12][0-9]|0[1-9]) (?:2[0-3]|1[0-9]|0[0-9]):[0-5][0-9]:[0-5][0-9].[0-9]{1,3}$/i"`;

View File

@ -20,13 +20,33 @@ import { jodaFormatToRegExp } from './joda-to-regexp';
describe('jodaFormatToRegExp', () => {
it('works for common formats', () => {
expect(jodaFormatToRegExp('d/M/yyyy').toString()).toMatchSnapshot();
expect(jodaFormatToRegExp('MM/dd/YYYY').toString()).toMatchSnapshot();
expect(jodaFormatToRegExp('M/d/YY').toString()).toMatchSnapshot();
expect(jodaFormatToRegExp('d-M-yyyy hh:mm:ss a').toString()).toMatchSnapshot();
expect(jodaFormatToRegExp('MM/dd/YYYY hh:mm:ss a').toString()).toMatchSnapshot();
expect(jodaFormatToRegExp('YYYY-MM-dd HH:mm:ss').toString()).toMatchSnapshot();
expect(jodaFormatToRegExp('YYYY-MM-dd HH:mm:ss.S').toString()).toMatchSnapshot();
expect(jodaFormatToRegExp('d/M/yyyy').toString()).toMatchInlineSnapshot(
`"/^(?:3[0-1]|[12][0-9]|[1-9])\\\\/(?:1[0-2]|[1-9])\\\\/[0-9]{4}$/i"`,
);
expect(jodaFormatToRegExp('MM/dd/YYYY').toString()).toMatchInlineSnapshot(
`"/^(?:1[0-2]|0[1-9])\\\\/(?:3[0-1]|[12][0-9]|0[1-9])\\\\/[0-9]{4}$/i"`,
);
expect(jodaFormatToRegExp('M/d/YY').toString()).toMatchInlineSnapshot(
`"/^(?:1[0-2]|[1-9])\\\\/(?:3[0-1]|[12][0-9]|[1-9])\\\\/[0-9]{2}$/i"`,
);
expect(jodaFormatToRegExp('d-M-yyyy hh:mm:ss a').toString()).toMatchInlineSnapshot(
`"/^(?:3[0-1]|[12][0-9]|[1-9])-(?:1[0-2]|[1-9])-[0-9]{4} (?:1[0-2]|0[1-9]):[0-5][0-9]:[0-5][0-9] [ap]m$/i"`,
);
expect(jodaFormatToRegExp('MM/dd/YYYY hh:mm:ss a').toString()).toMatchInlineSnapshot(
`"/^(?:1[0-2]|0[1-9])\\\\/(?:3[0-1]|[12][0-9]|0[1-9])\\\\/[0-9]{4} (?:1[0-2]|0[1-9]):[0-5][0-9]:[0-5][0-9] [ap]m$/i"`,
);
expect(jodaFormatToRegExp('YYYY-MM-dd HH:mm:ss').toString()).toMatchInlineSnapshot(
`"/^[0-9]{4}-(?:1[0-2]|0[1-9])-(?:3[0-1]|[12][0-9]|0[1-9]) (?:2[0-3]|1[0-9]|0[0-9]):[0-5][0-9]:[0-5][0-9]$/i"`,
);
expect(jodaFormatToRegExp('YYYY-MM-dd HH:mm:ss.S').toString()).toMatchInlineSnapshot(
`"/^[0-9]{4}-(?:1[0-2]|0[1-9])-(?:3[0-1]|[12][0-9]|0[1-9]) (?:2[0-3]|1[0-9]|0[0-9]):[0-5][0-9]:[0-5][0-9].[0-9]{1,3}$/i"`,
);
});
it('matches dates when needed', () => {

View File

@ -23,7 +23,7 @@ exports[`data source view matches snapshot 1`] = `
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
text="See in SQL view"
text="View SQL query for table"
/>
</Blueprint3.Menu>
}

View File

@ -471,7 +471,7 @@ GROUP BY 1`;
{!noSqlMode && (
<MenuItem
icon={IconNames.APPLICATION}
text="See in SQL view"
text="View SQL query for table"
onClick={() => goToQuery(DatasourcesView.DATASOURCE_SQL)}
/>
)}

View File

@ -4,152 +4,20 @@ exports[`home view matches snapshot 1`] = `
<div
className="home-view app-view"
>
<a
onClick={[Function]}
>
<Blueprint3.Card
className="home-view-card"
elevation={0}
interactive={true}
>
<Component>
<Blueprint3.Icon
color="#bfccd5"
icon="graph"
/>
 
Status
</Component>
<p>
Loading...
</p>
</Blueprint3.Card>
</a>
<a
href="#datasources"
>
<Blueprint3.Card
className="home-view-card"
elevation={0}
interactive={true}
>
<Component>
<Blueprint3.Icon
color="#bfccd5"
icon="multi-select"
/>
 
Datasources
</Component>
<p>
Loading...
</p>
</Blueprint3.Card>
</a>
<a
href="#segments"
>
<Blueprint3.Card
className="home-view-card"
elevation={0}
interactive={true}
>
<Component>
<Blueprint3.Icon
color="#bfccd5"
icon="stacked-chart"
/>
 
Segments
</Component>
<p>
Loading...
</p>
</Blueprint3.Card>
</a>
<a
href="#tasks"
>
<Blueprint3.Card
className="home-view-card"
elevation={0}
interactive={true}
>
<Component>
<Blueprint3.Icon
color="#bfccd5"
icon="list-columns"
/>
 
Supervisors
</Component>
<p>
Loading...
</p>
</Blueprint3.Card>
</a>
<a
href="#tasks"
>
<Blueprint3.Card
className="home-view-card"
elevation={0}
interactive={true}
>
<Component>
<Blueprint3.Icon
color="#bfccd5"
icon="gantt-chart"
/>
 
Tasks
</Component>
<p>
Loading...
</p>
</Blueprint3.Card>
</a>
<a
href="#servers"
>
<Blueprint3.Card
className="home-view-card"
elevation={0}
interactive={true}
>
<Component>
<Blueprint3.Icon
color="#bfccd5"
icon="database"
/>
 
Servers
</Component>
<p>
Loading...
</p>
</Blueprint3.Card>
</a>
<a
href="#lookups"
>
<Blueprint3.Card
className="home-view-card"
elevation={0}
interactive={true}
>
<Component>
<Blueprint3.Icon
color="#bfccd5"
icon="properties"
/>
 
Lookups
</Component>
<p>
Loading...
</p>
</Blueprint3.Card>
</a>
<StatusCard />
<DatasourcesCard
noSqlMode={false}
/>
<SegmentsCard
noSqlMode={false}
/>
<SupervisorsCard />
<TasksCard
noSqlMode={false}
/>
<ServersCard
noSqlMode={false}
/>
<LookupsCard />
</div>
`;

View File

@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`datasources card matches snapshot 1`] = `
<a
class="home-view-card datasources-card"
href="#datasources"
>
<div
class="bp3-card bp3-interactive bp3-elevation-0"
>
<h5
class="bp3-heading"
>
<span
class="bp3-icon bp3-icon-multi-select"
icon="multi-select"
>
<svg
data-icon="multi-select"
fill="#bfccd5"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
multi-select
</desc>
<path
d="M12 3.98H4c-.55 0-1 .45-1 1v1h8v5h1c.55 0 1-.45 1-1v-5c0-.55-.45-1-1-1zm3-3H7c-.55 0-1 .45-1 1v1h8v5h1c.55 0 1-.45 1-1v-5c0-.55-.45-1-1-1zm-6 6H1c-.55 0-1 .45-1 1v5c0 .55.45 1 1 1h8c.55 0 1-.45 1-1v-5c0-.55-.45-1-1-1zm-1 5H2v-3h6v3z"
fill-rule="evenodd"
/>
</svg>
</span>
 
Datasources
</h5>
<p>
Loading...
</p>
</div>
</a>
`;

View File

@ -0,0 +1,31 @@
/*
* 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 { DatasourcesCard } from './datasources-card';
describe('datasources card', () => {
it('matches snapshot', () => {
const datasourcesCard = <DatasourcesCard noSqlMode={false} />;
const { container } = render(datasourcesCard);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -0,0 +1,97 @@
/*
* 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 { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import React from 'react';
import { pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils';
import { HomeViewCard } from '../home-view-card/home-view-card';
export interface DatasourcesCardProps {
noSqlMode: boolean;
}
export interface DatasourcesCardState {
datasourceCountLoading: boolean;
datasourceCount: number;
datasourceCountError?: string;
}
export class DatasourcesCard extends React.PureComponent<
DatasourcesCardProps,
DatasourcesCardState
> {
private datasourceQueryManager: QueryManager<boolean, any>;
constructor(props: DatasourcesCardProps, context: any) {
super(props, context);
this.state = {
datasourceCountLoading: false,
datasourceCount: 0,
};
this.datasourceQueryManager = new QueryManager({
processQuery: async noSqlMode => {
let datasources: string[];
if (!noSqlMode) {
datasources = await queryDruidSql({
query: `SELECT datasource FROM sys.segments GROUP BY 1`,
});
} else {
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources');
datasources = datasourcesResp.data;
}
return datasources.length;
},
onStateChange: ({ result, loading, error }) => {
this.setState({
datasourceCountLoading: loading,
datasourceCount: result,
datasourceCountError: error || undefined,
});
},
});
}
componentDidMount(): void {
const { noSqlMode } = this.props;
this.datasourceQueryManager.runQuery(noSqlMode);
}
componentWillUnmount(): void {
this.datasourceQueryManager.terminate();
}
render(): JSX.Element {
const { datasourceCountLoading, datasourceCountError, datasourceCount } = this.state;
return (
<HomeViewCard
className="datasources-card"
href={'#datasources'}
icon={IconNames.MULTI_SELECT}
title={'Datasources'}
loading={datasourceCountLoading}
error={datasourceCountError}
>
{pluralIfNeeded(datasourceCount, 'datasource')}
</HomeViewCard>
);
}
}

View File

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`home view card matches snapshot 1`] = `
<a
class="home-view-card some-card"
href="#somewhere"
>
<div
class="bp3-card bp3-interactive bp3-elevation-0"
>
<h5
class="bp3-heading"
>
<span
class="bp3-icon bp3-icon-database"
icon="database"
>
<svg
data-icon="database"
fill="#bfccd5"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
database
</desc>
<path
d="M8 4c3.31 0 6-.9 6-2s-2.69-2-6-2C4.68 0 2 .9 2 2s2.68 2 6 2zm-6-.48V8c0 1.1 2.69 2 6 2s6-.9 6-2V3.52C12.78 4.4 10.56 5 8 5s-4.78-.6-6-1.48zm0 6V14c0 1.1 2.69 2 6 2s6-.9 6-2V9.52C12.78 10.4 10.56 11 8 11s-4.78-.6-6-1.48z"
fill-rule="evenodd"
/>
</svg>
</span>
 
Something
</h5>
Thigns
</div>
</a>
`;

View File

@ -0,0 +1,23 @@
/*
* 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.
*/
.home-view-card {
.bp3-card {
height: 170px;
}
}

View File

@ -0,0 +1,43 @@
/*
* 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 { IconNames } from '@blueprintjs/icons';
import { render } from '@testing-library/react';
import React from 'react';
import { HomeViewCard } from './home-view-card';
describe('home view card', () => {
it('matches snapshot', () => {
const homeViewCard = (
<HomeViewCard
className="some-card"
href={'#somewhere'}
icon={IconNames.DATABASE}
title={'Something'}
loading={false}
error={undefined}
>
Thigns
</HomeViewCard>
);
const { container } = render(homeViewCard);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -0,0 +1,56 @@
/*
* 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 { Card, H5, Icon, IconName } from '@blueprintjs/core';
import classNames from 'classnames';
import React from 'react';
import './home-view-card.scss';
export interface HomeViewCardProps {
className: string;
onClick?: () => void;
href?: string;
icon: IconName;
title: string;
loading: boolean;
error: string | undefined;
}
export class HomeViewCard extends React.PureComponent<HomeViewCardProps> {
render(): JSX.Element {
const { className, onClick, href, icon, title, loading, error, children } = this.props;
return (
<a
className={classNames('home-view-card', className)}
onClick={onClick}
href={href}
target={href && href[0] === '/' ? '_blank' : undefined}
>
<Card interactive>
<H5>
<Icon color="#bfccd5" icon={icon} />
&nbsp;{title}
</H5>
{loading ? <p>Loading...</p> : error ? `Error: ${error}` : children}
</Card>
</a>
);
}
}

View File

@ -27,8 +27,4 @@
text-decoration: inherit;
color: inherit;
}
.home-view-card {
height: 170px;
}
}

View File

@ -16,530 +16,35 @@
* limitations under the License.
*/
import { Card, H5, Icon } from '@blueprintjs/core';
import { IconName, IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import { sum } from 'd3-array';
import React from 'react';
import { StatusDialog } from '../../dialogs/status-dialog/status-dialog';
import { compact, lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from '../../utils';
import { deepGet } from '../../utils/object-change';
import { DatasourcesCard } from './datasources-card/datasources-card';
import { LookupsCard } from './lookups-card/lookups-card';
import { SegmentsCard } from './segments-card/segments-card';
import { ServersCard } from './servers-card/servers-card';
import { StatusCard } from './status-card/status-card';
import { SupervisorsCard } from './supervisors-card/supervisors-card';
import { TasksCard } from './tasks-card/tasks-card';
import './home-view.scss';
export interface CardOptions {
onClick?: () => void;
href?: string;
icon: IconName;
title: string;
loading?: boolean;
content: JSX.Element | string;
error?: string;
}
export interface HomeViewProps {
noSqlMode: boolean;
}
export interface HomeViewState {
versionLoading: boolean;
version: string;
versionError?: string;
datasourceCountLoading: boolean;
datasourceCount: number;
datasourceCountError?: string;
segmentCountLoading: boolean;
segmentCount: number;
unavailableSegmentCount: number;
segmentCountError?: string;
supervisorCountLoading: boolean;
runningSupervisorCount: number;
suspendedSupervisorCount: number;
supervisorCountError?: string;
taskCountLoading: boolean;
runningTaskCount: number;
pendingTaskCount: number;
successTaskCount: number;
failedTaskCount: number;
waitingTaskCount: number;
taskCountError?: string;
serverCountLoading: boolean;
coordinatorCount: number;
overlordCount: number;
routerCount: number;
brokerCount: number;
historicalCount: number;
middleManagerCount: number;
peonCount: number;
indexerCount: number;
serverCountError?: string;
showStatusDialog: boolean;
lookupsCountLoading: boolean;
lookupsCount: number;
lookupsUninitialized: boolean;
lookupsCountError?: string;
}
export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState> {
private versionQueryManager: QueryManager<null, string>;
private datasourceQueryManager: QueryManager<boolean, any>;
private segmentQueryManager: QueryManager<boolean, any>;
private supervisorQueryManager: QueryManager<null, any>;
private taskQueryManager: QueryManager<boolean, any>;
private serverQueryManager: QueryManager<boolean, any>;
private lookupsQueryManager: QueryManager<null, any>;
constructor(props: HomeViewProps, context: any) {
super(props, context);
this.state = {
versionLoading: true,
version: '',
datasourceCountLoading: false,
datasourceCount: 0,
segmentCountLoading: false,
segmentCount: 0,
unavailableSegmentCount: 0,
supervisorCountLoading: false,
runningSupervisorCount: 0,
suspendedSupervisorCount: 0,
taskCountLoading: false,
runningTaskCount: 0,
pendingTaskCount: 0,
successTaskCount: 0,
failedTaskCount: 0,
waitingTaskCount: 0,
serverCountLoading: false,
coordinatorCount: 0,
overlordCount: 0,
routerCount: 0,
brokerCount: 0,
historicalCount: 0,
middleManagerCount: 0,
peonCount: 0,
indexerCount: 0,
showStatusDialog: false,
lookupsCountLoading: false,
lookupsCount: 0,
lookupsUninitialized: false,
};
this.versionQueryManager = new QueryManager({
processQuery: async () => {
const statusResp = await axios.get('/status');
return statusResp.data.version;
},
onStateChange: ({ result, loading, error }) => {
this.setState({
versionLoading: loading,
version: result,
versionError: error,
});
},
});
this.datasourceQueryManager = new QueryManager({
processQuery: async noSqlMode => {
let datasources: string[];
if (!noSqlMode) {
datasources = await queryDruidSql({
query: `SELECT datasource FROM sys.segments GROUP BY 1`,
});
} else {
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources');
datasources = datasourcesResp.data;
}
return datasources.length;
},
onStateChange: ({ result, loading, error }) => {
this.setState({
datasourceCountLoading: loading,
datasourceCount: result,
datasourceCountError: error || undefined,
});
},
});
this.segmentQueryManager = new QueryManager({
processQuery: async noSqlMode => {
if (noSqlMode) {
const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
const loadstatus = loadstatusResp.data;
const unavailableSegmentNum = sum(Object.keys(loadstatus), key => loadstatus[key]);
const datasourcesMetaResp = await axios.get('/druid/coordinator/v1/datasources?simple');
const datasourcesMeta = datasourcesMetaResp.data;
const availableSegmentNum = sum(datasourcesMeta, (curr: any) =>
deepGet(curr, 'properties.segments.count'),
);
return {
count: availableSegmentNum + unavailableSegmentNum,
unavailable: unavailableSegmentNum,
};
} else {
const segments = await queryDruidSql({
query: `SELECT
COUNT(*) as "count",
COUNT(*) FILTER (WHERE is_available = 0) as "unavailable"
FROM sys.segments`,
});
return segments.length === 1 ? segments[0] : null;
}
},
onStateChange: ({ result, loading, error }) => {
this.setState({
segmentCountLoading: loading,
segmentCount: result ? result.count : 0,
unavailableSegmentCount: result ? result.unavailable : 0,
segmentCountError: error,
});
},
});
this.supervisorQueryManager = new QueryManager({
processQuery: async () => {
const resp = await axios.get('/druid/indexer/v1/supervisor?full');
const data = resp.data;
const runningSupervisorCount = data.filter((d: any) => d.spec.suspended === false).length;
const suspendedSupervisorCount = data.filter((d: any) => d.spec.suspended === true).length;
return {
runningSupervisorCount,
suspendedSupervisorCount,
};
},
onStateChange: ({ result, loading, error }) => {
this.setState({
runningSupervisorCount: result ? result.runningSupervisorCount : 0,
suspendedSupervisorCount: result ? result.suspendedSupervisorCount : 0,
supervisorCountLoading: loading,
supervisorCountError: error,
});
},
});
this.taskQueryManager = new QueryManager({
processQuery: async noSqlMode => {
if (noSqlMode) {
const completeTasksResp = await axios.get('/druid/indexer/v1/completeTasks');
const runningTasksResp = await axios.get('/druid/indexer/v1/runningTasks');
const pendingTasksResp = await axios.get('/druid/indexer/v1/pendingTasks');
const waitingTasksResp = await axios.get('/druid/indexer/v1/waitingTasks');
return {
SUCCESS: completeTasksResp.data.filter((d: any) => d.status === 'SUCCESS').length,
FAILED: completeTasksResp.data.filter((d: any) => d.status === 'FAILED').length,
RUNNING: runningTasksResp.data.length,
PENDING: pendingTasksResp.data.length,
WAITING: waitingTasksResp.data.length,
};
} else {
const taskCountsFromQuery: { status: string; count: number }[] = await queryDruidSql({
query: `SELECT
CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
COUNT (*) AS "count"
FROM sys.tasks
GROUP BY 1`,
});
return lookupBy(taskCountsFromQuery, x => x.status, x => x.count);
}
},
onStateChange: ({ result, loading, error }) => {
this.setState({
taskCountLoading: loading,
successTaskCount: result ? result.SUCCESS : 0,
failedTaskCount: result ? result.FAILED : 0,
runningTaskCount: result ? result.RUNNING : 0,
pendingTaskCount: result ? result.PENDING : 0,
waitingTaskCount: result ? result.WAITING : 0,
taskCountError: error,
});
},
});
this.serverQueryManager = new QueryManager({
processQuery: async noSqlMode => {
if (noSqlMode) {
const serversResp = await axios.get('/druid/coordinator/v1/servers?simple');
const middleManagerResp = await axios.get('/druid/indexer/v1/workers');
return {
historical: serversResp.data.filter((s: any) => s.type === 'historical').length,
middle_manager: middleManagerResp.data.length,
peon: serversResp.data.filter((s: any) => s.type === 'indexer-executor').length,
};
} else {
const serverCountsFromQuery: {
server_type: string;
count: number;
}[] = await queryDruidSql({
query: `SELECT server_type, COUNT(*) as "count" FROM sys.servers GROUP BY 1`,
});
return lookupBy(serverCountsFromQuery, x => x.server_type, x => x.count);
}
},
onStateChange: ({ result, loading, error }) => {
this.setState({
serverCountLoading: loading,
coordinatorCount: result ? result.coordinator : 0,
overlordCount: result ? result.overlord : 0,
routerCount: result ? result.router : 0,
brokerCount: result ? result.broker : 0,
historicalCount: result ? result.historical : 0,
middleManagerCount: result ? result.middle_manager : 0,
peonCount: result ? result.peon : 0,
indexerCount: result ? result.indexer : 0,
serverCountError: error,
});
},
});
this.lookupsQueryManager = new QueryManager({
processQuery: async () => {
const resp = await axios.get('/druid/coordinator/v1/lookups/status');
const data = resp.data;
const lookupsCount = sum(Object.keys(data).map(k => Object.keys(data[k]).length));
return {
lookupsCount,
};
},
onStateChange: ({ result, loading, error }) => {
this.setState({
lookupsCount: result ? result.lookupsCount : 0,
lookupsUninitialized: error === 'Request failed with status code 404',
lookupsCountLoading: loading,
lookupsCountError: error,
});
},
});
}
componentDidMount(): void {
const { noSqlMode } = this.props;
this.versionQueryManager.runQuery(null);
this.datasourceQueryManager.runQuery(noSqlMode);
this.segmentQueryManager.runQuery(noSqlMode);
this.supervisorQueryManager.runQuery(null);
this.taskQueryManager.runQuery(noSqlMode);
this.serverQueryManager.runQuery(noSqlMode);
this.lookupsQueryManager.runQuery(null);
}
componentWillUnmount(): void {
this.versionQueryManager.terminate();
this.datasourceQueryManager.terminate();
this.segmentQueryManager.terminate();
this.supervisorQueryManager.terminate();
this.taskQueryManager.terminate();
this.serverQueryManager.terminate();
}
renderStatusDialog() {
const { showStatusDialog } = this.state;
if (!showStatusDialog) {
return null;
}
return (
<StatusDialog
onClose={() => this.setState({ showStatusDialog: false })}
title={'Status'}
isOpen
/>
);
}
renderCard(cardOptions: CardOptions): JSX.Element {
return (
<a
onClick={cardOptions.onClick}
href={cardOptions.href}
target={cardOptions.href && cardOptions.href[0] === '/' ? '_blank' : undefined}
>
<Card className="home-view-card" interactive>
<H5>
<Icon color="#bfccd5" icon={cardOptions.icon} />
&nbsp;{cardOptions.title}
</H5>
{cardOptions.loading ? (
<p>Loading...</p>
) : cardOptions.error ? (
`Error: ${cardOptions.error}`
) : (
cardOptions.content
)}
</Card>
</a>
);
}
renderPluralIfNeededPair(
count1: number,
singular1: string,
count2: number,
singular2: string,
): JSX.Element | undefined {
const text = compact([
count1 ? pluralIfNeeded(count1, singular1) : undefined,
count2 ? pluralIfNeeded(count2, singular2) : undefined,
]).join(', ');
if (!text) return;
return <p>{text}</p>;
}
export class HomeView extends React.PureComponent<HomeViewProps> {
render(): JSX.Element {
const state = this.state;
const { noSqlMode } = this.props;
return (
<div className="home-view app-view">
{this.renderCard({
onClick: () => this.setState({ showStatusDialog: true }),
icon: IconNames.GRAPH,
title: 'Status',
loading: state.versionLoading,
content: state.version ? `Apache Druid is running version ${state.version}` : '',
error: state.versionError,
})}
{this.renderCard({
href: '#datasources',
icon: IconNames.MULTI_SELECT,
title: 'Datasources',
loading: state.datasourceCountLoading,
content: pluralIfNeeded(state.datasourceCount, 'datasource'),
error: state.datasourceCountError,
})}
{this.renderCard({
href: '#segments',
icon: IconNames.STACKED_CHART,
title: 'Segments',
loading: state.segmentCountLoading,
content: (
<>
<p>{pluralIfNeeded(state.segmentCount, 'segment')}</p>
{Boolean(state.unavailableSegmentCount) && (
<p>{pluralIfNeeded(state.unavailableSegmentCount, 'unavailable segment')}</p>
)}
</>
),
error: state.datasourceCountError,
})}
{this.renderCard({
href: '#tasks',
icon: IconNames.LIST_COLUMNS,
title: 'Supervisors',
loading: state.supervisorCountLoading,
content: (
<>
{!Boolean(state.runningSupervisorCount + state.suspendedSupervisorCount) && (
<p>0 supervisors</p>
)}
{Boolean(state.runningSupervisorCount) && (
<p>{pluralIfNeeded(state.runningSupervisorCount, 'running supervisor')}</p>
)}
{Boolean(state.suspendedSupervisorCount) && (
<p>{pluralIfNeeded(state.suspendedSupervisorCount, 'suspended supervisor')}</p>
)}
</>
),
error: state.supervisorCountError,
})}
{this.renderCard({
href: '#tasks',
icon: IconNames.GANTT_CHART,
title: 'Tasks',
loading: state.taskCountLoading,
content: (
<>
{Boolean(state.runningTaskCount) && (
<p>{pluralIfNeeded(state.runningTaskCount, 'running task')}</p>
)}
{Boolean(state.pendingTaskCount) && (
<p>{pluralIfNeeded(state.pendingTaskCount, 'pending task')}</p>
)}
{Boolean(state.successTaskCount) && (
<p>{pluralIfNeeded(state.successTaskCount, 'successful task')}</p>
)}
{Boolean(state.waitingTaskCount) && (
<p>{pluralIfNeeded(state.waitingTaskCount, 'waiting task')}</p>
)}
{Boolean(state.failedTaskCount) && (
<p>{pluralIfNeeded(state.failedTaskCount, 'failed task')}</p>
)}
{!(
Boolean(state.runningTaskCount) ||
Boolean(state.pendingTaskCount) ||
Boolean(state.successTaskCount) ||
Boolean(state.waitingTaskCount) ||
Boolean(state.failedTaskCount)
) && <p>There are no tasks</p>}
</>
),
error: state.taskCountError,
})}
{this.renderCard({
href: '#servers',
icon: IconNames.DATABASE,
title: 'Servers',
loading: state.serverCountLoading,
content: (
<>
{this.renderPluralIfNeededPair(
state.overlordCount,
'overlord',
state.coordinatorCount,
'coordinator',
)}
{this.renderPluralIfNeededPair(
state.routerCount,
'router',
state.brokerCount,
'broker',
)}
{this.renderPluralIfNeededPair(
state.historicalCount,
'historical',
state.middleManagerCount,
'middle manager',
)}
{this.renderPluralIfNeededPair(
state.peonCount,
'peon',
state.indexerCount,
'indexer',
)}
</>
),
error: state.serverCountError ? state.serverCountError : undefined,
})}
{this.renderCard({
href: '#lookups',
icon: IconNames.PROPERTIES,
title: 'Lookups',
loading: state.lookupsCountLoading,
content: (
<>
<p>
{!state.lookupsUninitialized
? pluralIfNeeded(state.lookupsCount, 'lookup')
: 'Lookups uninitialized'}
</p>
</>
),
error: !state.lookupsUninitialized ? state.lookupsCountError : undefined,
})}
{!state.versionLoading && this.renderStatusDialog()}
<StatusCard />
<DatasourcesCard noSqlMode={noSqlMode} />
<SegmentsCard noSqlMode={noSqlMode} />
<SupervisorsCard />
<TasksCard noSqlMode={noSqlMode} />
<ServersCard noSqlMode={noSqlMode} />
<LookupsCard />
</div>
);
}

View File

@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lookups card matches snapshot 1`] = `
<a
class="home-view-card lookups-card"
href="#lookups"
>
<div
class="bp3-card bp3-interactive bp3-elevation-0"
>
<h5
class="bp3-heading"
>
<span
class="bp3-icon bp3-icon-properties"
icon="properties"
>
<svg
data-icon="properties"
fill="#bfccd5"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
properties
</desc>
<path
d="M2 6C.9 6 0 6.9 0 8s.9 2 2 2 2-.9 2-2-.9-2-2-2zm4-3h9c.55 0 1-.45 1-1s-.45-1-1-1H6c-.55 0-1 .45-1 1s.45 1 1 1zm-4 9c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm13-5H6c-.55 0-1 .45-1 1s.45 1 1 1h9c.55 0 1-.45 1-1s-.45-1-1-1zm0 6H6c-.55 0-1 .45-1 1s.45 1 1 1h9c.55 0 1-.45 1-1s-.45-1-1-1zM2 0C.9 0 0 .9 0 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
fill-rule="evenodd"
/>
</svg>
</span>
 
Lookups
</h5>
<p>
Loading...
</p>
</div>
</a>
`;

View File

@ -0,0 +1,31 @@
/*
* 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 { LookupsCard } from './lookups-card';
describe('lookups card', () => {
it('matches snapshot', () => {
const lookupsCard = <LookupsCard />;
const { container } = render(lookupsCard);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -0,0 +1,98 @@
/*
* 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 { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import { sum } from 'd3-array';
import React from 'react';
import { pluralIfNeeded, QueryManager } from '../../../utils';
import { HomeViewCard } from '../home-view-card/home-view-card';
export interface LookupsCardProps {}
export interface LookupsCardState {
lookupsCountLoading: boolean;
lookupsCount: number;
lookupsUninitialized: boolean;
lookupsCountError?: string;
}
export class LookupsCard extends React.PureComponent<LookupsCardProps, LookupsCardState> {
private lookupsQueryManager: QueryManager<null, any>;
constructor(props: LookupsCardProps, context: any) {
super(props, context);
this.state = {
lookupsCountLoading: false,
lookupsCount: 0,
lookupsUninitialized: false,
};
this.lookupsQueryManager = new QueryManager({
processQuery: async () => {
const resp = await axios.get('/druid/coordinator/v1/lookups/status');
const data = resp.data;
const lookupsCount = sum(Object.keys(data).map(k => Object.keys(data[k]).length));
return {
lookupsCount,
};
},
onStateChange: ({ result, loading, error }) => {
this.setState({
lookupsCount: result ? result.lookupsCount : 0,
lookupsUninitialized: error === 'Request failed with status code 404',
lookupsCountLoading: loading,
lookupsCountError: error,
});
},
});
}
componentDidMount(): void {
this.lookupsQueryManager.runQuery(null);
}
componentWillUnmount(): void {
this.lookupsQueryManager.terminate();
}
render(): JSX.Element {
const {
lookupsCountLoading,
lookupsCount,
lookupsUninitialized,
lookupsCountError,
} = this.state;
return (
<HomeViewCard
className="lookups-card"
href={'#lookups'}
icon={IconNames.PROPERTIES}
title={'Lookups'}
loading={lookupsCountLoading}
error={!lookupsUninitialized ? lookupsCountError : undefined}
>
<p>
{!lookupsUninitialized ? pluralIfNeeded(lookupsCount, 'lookup') : 'Lookups uninitialized'}
</p>
</HomeViewCard>
);
}
}

View File

@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`segments card matches snapshot 1`] = `
<a
class="home-view-card segments-card"
href="#segments"
>
<div
class="bp3-card bp3-interactive bp3-elevation-0"
>
<h5
class="bp3-heading"
>
<span
class="bp3-icon bp3-icon-stacked-chart"
icon="stacked-chart"
>
<svg
data-icon="stacked-chart"
fill="#bfccd5"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
stacked-chart
</desc>
<path
d="M10 2c0-.55-.45-1-1-1H8c-.55 0-1 .45-1 1v3h3V2zm3 10h1c.55 0 1-.45 1-1V8h-3v3c0 .55.45 1 1 1zm2-7c0-.55-.45-1-1-1h-1c-.55 0-1 .45-1 1v2h3V5zm-5 1H7v3h3V6zM5 7c0-.55-.45-1-1-1H3c-.55 0-1 .45-1 1v1h3V7zm3 5h1c.55 0 1-.45 1-1v-1H7v1c0 .55.45 1 1 1zm7 1H2c-.55 0-1 .45-1 1s.45 1 1 1h13c.55 0 1-.45 1-1s-.45-1-1-1zM3 12h1c.55 0 1-.45 1-1V9H2v2c0 .55.45 1 1 1z"
fill-rule="evenodd"
/>
</svg>
</span>
 
Segments
</h5>
<p>
Loading...
</p>
</div>
</a>
`;

View File

@ -0,0 +1,31 @@
/*
* 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 { SegmentsCard } from './segments-card';
describe('segments card', () => {
it('matches snapshot', () => {
const segmentsCard = <SegmentsCard noSqlMode={false} />;
const { container } = render(segmentsCard);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -0,0 +1,122 @@
/*
* 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 { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import { sum } from 'd3-array';
import React from 'react';
import { pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils';
import { deepGet } from '../../../utils/object-change';
import { HomeViewCard } from '../home-view-card/home-view-card';
export interface SegmentsCardProps {
noSqlMode: boolean;
}
export interface SegmentsCardState {
segmentCountLoading: boolean;
segmentCount: number;
unavailableSegmentCount: number;
segmentCountError?: string;
}
export class SegmentsCard extends React.PureComponent<SegmentsCardProps, SegmentsCardState> {
private segmentQueryManager: QueryManager<boolean, any>;
constructor(props: SegmentsCardProps, context: any) {
super(props, context);
this.state = {
segmentCountLoading: false,
segmentCount: 0,
unavailableSegmentCount: 0,
};
this.segmentQueryManager = new QueryManager({
processQuery: async noSqlMode => {
if (noSqlMode) {
const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
const loadstatus = loadstatusResp.data;
const unavailableSegmentNum = sum(Object.keys(loadstatus), key => loadstatus[key]);
const datasourcesMetaResp = await axios.get('/druid/coordinator/v1/datasources?simple');
const datasourcesMeta = datasourcesMetaResp.data;
const availableSegmentNum = sum(datasourcesMeta, (curr: any) =>
deepGet(curr, 'properties.segments.count'),
);
return {
count: availableSegmentNum + unavailableSegmentNum,
unavailable: unavailableSegmentNum,
};
} else {
const segments = await queryDruidSql({
query: `SELECT
COUNT(*) as "count",
COUNT(*) FILTER (WHERE is_available = 0) as "unavailable"
FROM sys.segments`,
});
return segments.length === 1 ? segments[0] : null;
}
},
onStateChange: ({ result, loading, error }) => {
this.setState({
segmentCountLoading: loading,
segmentCount: result ? result.count : 0,
unavailableSegmentCount: result ? result.unavailable : 0,
segmentCountError: error,
});
},
});
}
componentDidMount(): void {
const { noSqlMode } = this.props;
this.segmentQueryManager.runQuery(noSqlMode);
}
componentWillUnmount(): void {
this.segmentQueryManager.terminate();
}
render(): JSX.Element {
const {
segmentCountLoading,
segmentCountError,
segmentCount,
unavailableSegmentCount,
} = this.state;
return (
<HomeViewCard
className="segments-card"
href={'#segments'}
icon={IconNames.STACKED_CHART}
title={'Segments'}
loading={segmentCountLoading}
error={segmentCountError}
>
<p>{pluralIfNeeded(segmentCount, 'segment')}</p>
{Boolean(unavailableSegmentCount) && (
<p>{pluralIfNeeded(unavailableSegmentCount, 'unavailable segment')}</p>
)}
</HomeViewCard>
);
}
}

View File

@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`servers card matches snapshot 1`] = `
<a
class="home-view-card servers-card"
href="#servers"
>
<div
class="bp3-card bp3-interactive bp3-elevation-0"
>
<h5
class="bp3-heading"
>
<span
class="bp3-icon bp3-icon-database"
icon="database"
>
<svg
data-icon="database"
fill="#bfccd5"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
database
</desc>
<path
d="M8 4c3.31 0 6-.9 6-2s-2.69-2-6-2C4.68 0 2 .9 2 2s2.68 2 6 2zm-6-.48V8c0 1.1 2.69 2 6 2s6-.9 6-2V3.52C12.78 4.4 10.56 5 8 5s-4.78-.6-6-1.48zm0 6V14c0 1.1 2.69 2 6 2s6-.9 6-2V9.52C12.78 10.4 10.56 11 8 11s-4.78-.6-6-1.48z"
fill-rule="evenodd"
/>
</svg>
</span>
 
Servers
</h5>
<p>
Loading...
</p>
</div>
</a>
`;

View File

@ -0,0 +1,31 @@
/*
* 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 { ServersCard } from './servers-card';
describe('servers card', () => {
it('matches snapshot', () => {
const serversCard = <ServersCard noSqlMode={false} />;
const { container } = render(serversCard);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -0,0 +1,160 @@
/*
* 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 { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import React from 'react';
import { compact, lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils';
import { HomeViewCard } from '../home-view-card/home-view-card';
export interface ServersCardProps {
noSqlMode: boolean;
}
export interface ServersCardState {
serverCountLoading: boolean;
coordinatorCount: number;
overlordCount: number;
routerCount: number;
brokerCount: number;
historicalCount: number;
middleManagerCount: number;
peonCount: number;
indexerCount: number;
serverCountError?: string;
}
export class ServersCard extends React.PureComponent<ServersCardProps, ServersCardState> {
static renderPluralIfNeededPair(
count1: number,
singular1: string,
count2: number,
singular2: string,
): JSX.Element | undefined {
const text = compact([
count1 ? pluralIfNeeded(count1, singular1) : undefined,
count2 ? pluralIfNeeded(count2, singular2) : undefined,
]).join(', ');
if (!text) return;
return <p>{text}</p>;
}
private serverQueryManager: QueryManager<boolean, any>;
constructor(props: ServersCardProps, context: any) {
super(props, context);
this.state = {
serverCountLoading: false,
coordinatorCount: 0,
overlordCount: 0,
routerCount: 0,
brokerCount: 0,
historicalCount: 0,
middleManagerCount: 0,
peonCount: 0,
indexerCount: 0,
};
this.serverQueryManager = new QueryManager({
processQuery: async noSqlMode => {
if (noSqlMode) {
const serversResp = await axios.get('/druid/coordinator/v1/servers?simple');
const middleManagerResp = await axios.get('/druid/indexer/v1/workers');
return {
historical: serversResp.data.filter((s: any) => s.type === 'historical').length,
middle_manager: middleManagerResp.data.length,
peon: serversResp.data.filter((s: any) => s.type === 'indexer-executor').length,
};
} else {
const serverCountsFromQuery: {
server_type: string;
count: number;
}[] = await queryDruidSql({
query: `SELECT server_type, COUNT(*) as "count" FROM sys.servers GROUP BY 1`,
});
return lookupBy(serverCountsFromQuery, x => x.server_type, x => x.count);
}
},
onStateChange: ({ result, loading, error }) => {
this.setState({
serverCountLoading: loading,
coordinatorCount: result ? result.coordinator : 0,
overlordCount: result ? result.overlord : 0,
routerCount: result ? result.router : 0,
brokerCount: result ? result.broker : 0,
historicalCount: result ? result.historical : 0,
middleManagerCount: result ? result.middle_manager : 0,
peonCount: result ? result.peon : 0,
indexerCount: result ? result.indexer : 0,
serverCountError: error,
});
},
});
}
componentDidMount(): void {
const { noSqlMode } = this.props;
this.serverQueryManager.runQuery(noSqlMode);
}
componentWillUnmount(): void {
this.serverQueryManager.terminate();
}
render(): JSX.Element {
const {
serverCountLoading,
coordinatorCount,
overlordCount,
routerCount,
brokerCount,
historicalCount,
middleManagerCount,
peonCount,
indexerCount,
serverCountError,
} = this.state;
return (
<HomeViewCard
className="servers-card"
href={'#servers'}
icon={IconNames.DATABASE}
title={'Servers'}
loading={serverCountLoading}
error={serverCountError}
>
{ServersCard.renderPluralIfNeededPair(
overlordCount,
'overlord',
coordinatorCount,
'coordinator',
)}
{ServersCard.renderPluralIfNeededPair(routerCount, 'router', brokerCount, 'broker')}
{ServersCard.renderPluralIfNeededPair(
historicalCount,
'historical',
middleManagerCount,
'middle manager',
)}
{ServersCard.renderPluralIfNeededPair(peonCount, 'peon', indexerCount, 'indexer')}
</HomeViewCard>
);
}
}

View File

@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`status card matches snapshot 1`] = `
<a
class="home-view-card status-card"
>
<div
class="bp3-card bp3-interactive bp3-elevation-0"
>
<h5
class="bp3-heading"
>
<span
class="bp3-icon bp3-icon-graph"
icon="graph"
>
<svg
data-icon="graph"
fill="#bfccd5"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
graph
</desc>
<path
d="M14 3c-1.06 0-1.92.83-1.99 1.88l-1.93.97A2.95 2.95 0 008 5c-.56 0-1.08.16-1.52.43L3.97 3.34C3.98 3.23 4 3.12 4 3c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2c.24 0 .47-.05.68-.13l2.51 2.09C5.08 7.29 5 7.63 5 8c0 .96.46 1.81 1.16 2.35l-.56 1.69c-.91.19-1.6.99-1.6 1.96 0 1.1.9 2 2 2s2-.9 2-2c0-.51-.2-.97-.51-1.32l.56-1.69A2.99 2.99 0 0011 8c0-.12-.02-.24-.04-.36l1.94-.97c.32.21.69.33 1.1.33 1.1 0 2-.9 2-2s-.9-2-2-2z"
fill-rule="evenodd"
/>
</svg>
</span>
 
Status
</h5>
<p>
Loading...
</p>
</div>
</a>
`;

View File

@ -0,0 +1,31 @@
/*
* 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 { StatusCard } from './status-card';
describe('status card', () => {
it('matches snapshot', () => {
const statusCard = <StatusCard />;
const { container } = render(statusCard);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -0,0 +1,103 @@
/*
* 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 { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import React from 'react';
import { StatusDialog } from '../../../dialogs/status-dialog/status-dialog';
import { QueryManager } from '../../../utils';
import { HomeViewCard } from '../home-view-card/home-view-card';
export interface StatusCardProps {}
export interface StatusCardState {
versionLoading: boolean;
version: string;
versionError?: string;
showStatusDialog: boolean;
}
export class StatusCard extends React.PureComponent<StatusCardProps, StatusCardState> {
private versionQueryManager: QueryManager<null, string>;
constructor(props: StatusCardProps, context: any) {
super(props, context);
this.state = {
versionLoading: true,
version: '',
showStatusDialog: false,
};
this.versionQueryManager = new QueryManager({
processQuery: async () => {
const statusResp = await axios.get('/status');
return statusResp.data.version;
},
onStateChange: ({ result, loading, error }) => {
this.setState({
versionLoading: loading,
version: result,
versionError: error,
});
},
});
}
componentDidMount(): void {
this.versionQueryManager.runQuery(null);
}
componentWillUnmount(): void {
this.versionQueryManager.terminate();
}
renderStatusDialog() {
const { showStatusDialog } = this.state;
if (!showStatusDialog) {
return null;
}
return (
<StatusDialog
onClose={() => this.setState({ showStatusDialog: false })}
title={'Status'}
isOpen
/>
);
}
render(): JSX.Element {
const { version, versionLoading, versionError } = this.state;
return (
<HomeViewCard
className="status-card"
onClick={() => this.setState({ showStatusDialog: true })}
icon={IconNames.GRAPH}
title="Status"
loading={versionLoading}
error={versionError}
>
{version ? `Apache Druid is running version ${version}` : ''}
{this.renderStatusDialog()}
</HomeViewCard>
);
}
}

View File

@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`supervisors card matches snapshot 1`] = `
<a
class="home-view-card supervisors-card"
href="#tasks"
>
<div
class="bp3-card bp3-interactive bp3-elevation-0"
>
<h5
class="bp3-heading"
>
<span
class="bp3-icon bp3-icon-list-columns"
icon="list-columns"
>
<svg
data-icon="list-columns"
fill="#bfccd5"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
list-columns
</desc>
<path
d="M6 1c.55 0 1 .45 1 1s-.45 1-1 1H1c-.55 0-1-.45-1-1s.45-1 1-1h5zm0 4c.55 0 1 .45 1 1s-.45 1-1 1H1c-.55 0-1-.45-1-1s.45-1 1-1h5zm0 4c.55 0 1 .45 1 1s-.45 1-1 1H1c-.55 0-1-.45-1-1s.45-1 1-1h5zm0 4c.55 0 1 .45 1 1s-.45 1-1 1H1c-.55 0-1-.45-1-1s.45-1 1-1h5zm9-12c.55 0 1 .45 1 1s-.45 1-1 1h-5c-.55 0-1-.45-1-1s.45-1 1-1h5zm0 4c.55 0 1 .45 1 1s-.45 1-1 1h-5c-.55 0-1-.45-1-1s.45-1 1-1h5zm0 4c.55 0 1 .45 1 1s-.45 1-1 1h-5c-.55 0-1-.45-1-1s.45-1 1-1h5zm0 4c.55 0 1 .45 1 1s-.45 1-1 1h-5c-.55 0-1-.45-1-1s.45-1 1-1h5z"
fill-rule="evenodd"
/>
</svg>
</span>
 
Supervisors
</h5>
<p>
Loading...
</p>
</div>
</a>
`;

View File

@ -0,0 +1,31 @@
/*
* 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 { SupervisorsCard } from './supervisors-card';
describe('supervisors card', () => {
it('matches snapshot', () => {
const supervisorsCard = <SupervisorsCard />;
const { container } = render(supervisorsCard);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -0,0 +1,106 @@
/*
* 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 { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import React from 'react';
import { pluralIfNeeded, QueryManager } from '../../../utils';
import { HomeViewCard } from '../home-view-card/home-view-card';
export interface SupervisorsCardProps {}
export interface SupervisorsCardState {
supervisorCountLoading: boolean;
runningSupervisorCount: number;
suspendedSupervisorCount: number;
supervisorCountError?: string;
}
export class SupervisorsCard extends React.PureComponent<
SupervisorsCardProps,
SupervisorsCardState
> {
private supervisorQueryManager: QueryManager<null, any>;
constructor(props: SupervisorsCardProps, context: any) {
super(props, context);
this.state = {
supervisorCountLoading: false,
runningSupervisorCount: 0,
suspendedSupervisorCount: 0,
};
this.supervisorQueryManager = new QueryManager({
processQuery: async () => {
const resp = await axios.get('/druid/indexer/v1/supervisor?full');
const data = resp.data;
const runningSupervisorCount = data.filter((d: any) => d.spec.suspended === false).length;
const suspendedSupervisorCount = data.filter((d: any) => d.spec.suspended === true).length;
return {
runningSupervisorCount,
suspendedSupervisorCount,
};
},
onStateChange: ({ result, loading, error }) => {
this.setState({
runningSupervisorCount: result ? result.runningSupervisorCount : 0,
suspendedSupervisorCount: result ? result.suspendedSupervisorCount : 0,
supervisorCountLoading: loading,
supervisorCountError: error,
});
},
});
}
componentDidMount(): void {
this.supervisorQueryManager.runQuery(null);
}
componentWillUnmount(): void {
this.supervisorQueryManager.terminate();
}
render(): JSX.Element {
const {
supervisorCountLoading,
supervisorCountError,
runningSupervisorCount,
suspendedSupervisorCount,
} = this.state;
return (
<HomeViewCard
className="supervisors-card"
href={'#tasks'}
icon={IconNames.LIST_COLUMNS}
title={'Supervisors'}
loading={supervisorCountLoading}
error={supervisorCountError}
>
{!Boolean(runningSupervisorCount + suspendedSupervisorCount) && <p>No supervisors</p>}
{Boolean(runningSupervisorCount) && (
<p>{pluralIfNeeded(runningSupervisorCount, 'running supervisor')}</p>
)}
{Boolean(suspendedSupervisorCount) && (
<p>{pluralIfNeeded(suspendedSupervisorCount, 'suspended supervisor')}</p>
)}
</HomeViewCard>
);
}
}

View File

@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`tasks card matches snapshot 1`] = `
<a
class="home-view-card tasks-card"
href="#tasks"
>
<div
class="bp3-card bp3-interactive bp3-elevation-0"
>
<h5
class="bp3-heading"
>
<span
class="bp3-icon bp3-icon-gantt-chart"
icon="gantt-chart"
>
<svg
data-icon="gantt-chart"
fill="#bfccd5"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
gantt-chart
</desc>
<path
d="M10 10c0 .55.45 1 1 1h4c.55 0 1-.45 1-1s-.45-1-1-1h-4c-.55 0-1 .45-1 1zM6 7c0 .55.45 1 1 1h4c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1zm9 5H2V3c0-.55-.45-1-1-1s-1 .45-1 1v10c0 .55.45 1 1 1h14c.55 0 1-.45 1-1s-.45-1-1-1zM4 5h3c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1z"
fill-rule="evenodd"
/>
</svg>
</span>
 
Tasks
</h5>
<p>
Loading...
</p>
</div>
</a>
`;

View File

@ -0,0 +1,31 @@
/*
* 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 { TasksCard } from './tasks-card';
describe('tasks card', () => {
it('matches snapshot', () => {
const tasksCard = <TasksCard noSqlMode={false} />;
const { container } = render(tasksCard);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -0,0 +1,138 @@
/*
* 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 { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import React from 'react';
import { lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils';
import { HomeViewCard } from '../home-view-card/home-view-card';
export interface TasksCardProps {
noSqlMode: boolean;
}
export interface TasksCardState {
taskCountLoading: boolean;
runningTaskCount: number;
pendingTaskCount: number;
successTaskCount: number;
failedTaskCount: number;
waitingTaskCount: number;
taskCountError?: string;
}
export class TasksCard extends React.PureComponent<TasksCardProps, TasksCardState> {
private taskQueryManager: QueryManager<boolean, any>;
constructor(props: TasksCardProps, context: any) {
super(props, context);
this.state = {
taskCountLoading: false,
runningTaskCount: 0,
pendingTaskCount: 0,
successTaskCount: 0,
failedTaskCount: 0,
waitingTaskCount: 0,
};
this.taskQueryManager = new QueryManager({
processQuery: async noSqlMode => {
if (noSqlMode) {
const completeTasksResp = await axios.get('/druid/indexer/v1/completeTasks');
const runningTasksResp = await axios.get('/druid/indexer/v1/runningTasks');
const pendingTasksResp = await axios.get('/druid/indexer/v1/pendingTasks');
const waitingTasksResp = await axios.get('/druid/indexer/v1/waitingTasks');
return {
SUCCESS: completeTasksResp.data.filter((d: any) => d.status === 'SUCCESS').length,
FAILED: completeTasksResp.data.filter((d: any) => d.status === 'FAILED').length,
RUNNING: runningTasksResp.data.length,
PENDING: pendingTasksResp.data.length,
WAITING: waitingTasksResp.data.length,
};
} else {
const taskCountsFromQuery: { status: string; count: number }[] = await queryDruidSql({
query: `SELECT
CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
COUNT (*) AS "count"
FROM sys.tasks
GROUP BY 1`,
});
return lookupBy(taskCountsFromQuery, x => x.status, x => x.count);
}
},
onStateChange: ({ result, loading, error }) => {
this.setState({
taskCountLoading: loading,
successTaskCount: result ? result.SUCCESS : 0,
failedTaskCount: result ? result.FAILED : 0,
runningTaskCount: result ? result.RUNNING : 0,
pendingTaskCount: result ? result.PENDING : 0,
waitingTaskCount: result ? result.WAITING : 0,
taskCountError: error,
});
},
});
}
componentDidMount(): void {
const { noSqlMode } = this.props;
this.taskQueryManager.runQuery(noSqlMode);
}
componentWillUnmount(): void {
this.taskQueryManager.terminate();
}
render(): JSX.Element {
const {
taskCountError,
taskCountLoading,
runningTaskCount,
pendingTaskCount,
successTaskCount,
failedTaskCount,
waitingTaskCount,
} = this.state;
return (
<HomeViewCard
className="tasks-card"
href={'#tasks'}
icon={IconNames.GANTT_CHART}
title={'Tasks'}
loading={taskCountLoading}
error={taskCountError}
>
{Boolean(runningTaskCount) && <p>{pluralIfNeeded(runningTaskCount, 'running task')}</p>}
{Boolean(pendingTaskCount) && <p>{pluralIfNeeded(pendingTaskCount, 'pending task')}</p>}
{Boolean(successTaskCount) && <p>{pluralIfNeeded(successTaskCount, 'successful task')}</p>}
{Boolean(waitingTaskCount) && <p>{pluralIfNeeded(waitingTaskCount, 'waiting task')}</p>}
{Boolean(failedTaskCount) && <p>{pluralIfNeeded(failedTaskCount, 'failed task')}</p>}
{!(
Boolean(runningTaskCount) ||
Boolean(pendingTaskCount) ||
Boolean(successTaskCount) ||
Boolean(waitingTaskCount) ||
Boolean(failedTaskCount)
) && <p>There are no tasks</p>}
</HomeViewCard>
);
}
}

View File

@ -12,6 +12,23 @@ exports[`segments-view matches snapshot 1`] = `
localStorageKey="segments-refresh-rate"
onRefresh={[Function]}
/>
<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>
<Blueprint3.Popover
boundary="scrollParent"
captureDismiss={false}
@ -24,7 +41,7 @@ exports[`segments-view matches snapshot 1`] = `
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
text="See in SQL view"
text="View SQL query for table"
/>
</Blueprint3.Menu>
}
@ -49,23 +66,6 @@ exports[`segments-view matches snapshot 1`] = `
icon="more"
/>
</Blueprint3.Popover>
<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 [

View File

@ -37,6 +37,7 @@ import { AsyncActionDialog } from '../../dialogs';
import { SegmentTableActionDialog } from '../../dialogs/segments-table-action-dialog/segment-table-action-dialog';
import {
addFilter,
compact,
filterMap,
formatBytes,
formatNumber,
@ -180,24 +181,28 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
});
let queryParts: string[];
let whereClause = '';
if (whereParts.length) {
whereClause = whereParts.join(' AND ');
}
if (query.groupByInterval) {
queryParts = [
queryParts = compact([
`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 '));
}
` ("start" || '/' || "end") IN (`,
` SELECT "start" || '/' || "end"`,
` FROM sys.segments`,
whereClause ? ` WHERE ${whereClause}` : '',
` GROUP BY 1`,
` LIMIT ${totalQuerySize}`,
` )`,
whereClause ? ` AND ${whereClause}` : '',
]);
if (query.sorted.length) {
queryParts.push(
@ -215,8 +220,8 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
`FROM sys.segments`,
];
if (whereParts.length) {
queryParts.push('WHERE ' + whereParts.join(' AND '));
if (whereClause) {
queryParts.push(`WHERE ${whereClause}`);
}
if (query.sorted.length) {
@ -625,7 +630,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
{!noSqlMode && (
<MenuItem
icon={IconNames.APPLICATION}
text="See in SQL view"
text="View SQL query for table"
disabled={!lastSegmentsQuery}
onClick={() => {
if (!lastSegmentsQuery) return;
@ -667,7 +672,6 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
}
localStorageKey={LocalStorageKeys.SEGMENTS_REFRESH_RATE}
/>
{this.renderBulkSegmentsActions()}
<Label>Group by</Label>
<ButtonGroup>
<Button
@ -689,6 +693,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
Interval
</Button>
</ButtonGroup>
{this.renderBulkSegmentsActions()}
<TableColumnSelector
columns={noSqlMode ? tableColumnsNoSql : tableColumns}
onChange={column => this.setState({ hiddenColumns: hiddenColumns.toggle(column) })}

View File

@ -46,7 +46,7 @@ exports[`servers view action servers view 1`] = `
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
text="See in SQL view"
text="View SQL query for table"
/>
</Blueprint3.Menu>
}

View File

@ -627,7 +627,7 @@ ORDER BY "rank" DESC, "server" DESC`;
{!noSqlMode && (
<MenuItem
icon={IconNames.APPLICATION}
text="See in SQL view"
text="View SQL query for table"
onClick={() => goToQuery(ServersView.SERVER_SQL)}
/>
)}

View File

@ -367,43 +367,6 @@ exports[`tasks view matches snapshot 1`] = `
localStorageKey="task-refresh-rate"
onRefresh={[Function]}
/>
<Blueprint3.Popover
boundary="scrollParent"
captureDismiss={false}
content={
<Blueprint3.Menu>
<Blueprint3.MenuItem
disabled={false}
icon="application"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
text="See in SQL view"
/>
</Blueprint3.Menu>
}
defaultIsOpen={false}
disabled={false}
fill={false}
hasBackdrop={false}
hoverCloseDelay={300}
hoverOpenDelay={150}
inheritDarkTheme={true}
interactionKind="click"
minimal={false}
modifiers={Object {}}
openOnTargetFocus={true}
position="bottom-left"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
>
<Blueprint3.Button
icon="more"
/>
</Blueprint3.Popover>
<Blueprint3.Popover
boundary="scrollParent"
captureDismiss={false}
@ -451,6 +414,43 @@ exports[`tasks view matches snapshot 1`] = `
text="Submit task"
/>
</Blueprint3.Popover>
<Blueprint3.Popover
boundary="scrollParent"
captureDismiss={false}
content={
<Blueprint3.Menu>
<Blueprint3.MenuItem
disabled={false}
icon="application"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
text="View SQL query for table"
/>
</Blueprint3.Menu>
}
defaultIsOpen={false}
disabled={false}
fill={false}
hasBackdrop={false}
hoverCloseDelay={300}
hoverOpenDelay={150}
inheritDarkTheme={true}
interactionKind="click"
minimal={false}
modifiers={Object {}}
openOnTargetFocus={true}
position="bottom-left"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
>
<Blueprint3.Button
icon="more"
/>
</Blueprint3.Popover>
<TableColumnSelector
columns={
Array [

View File

@ -1018,7 +1018,7 @@ ORDER BY "rank" DESC, "created_time" DESC`;
{!noSqlMode && (
<MenuItem
icon={IconNames.APPLICATION}
text="See in SQL view"
text="View SQL query for table"
onClick={() => goToQuery(TasksView.TASK_SQL)}
/>
)}
@ -1146,10 +1146,10 @@ ORDER BY "rank" DESC, "created_time" DESC`;
localStorageKey={LocalStorageKeys.TASKS_REFRESH_RATE}
onRefresh={auto => this.taskQueryManager.rerunLastQuery(auto)}
/>
{this.renderBulkTasksActions()}
<Popover content={submitTaskMenu} position={Position.BOTTOM_LEFT}>
<Button icon={IconNames.PLUS} text="Submit task" />
</Popover>
{this.renderBulkTasksActions()}
<TableColumnSelector
columns={taskTableColumns}
onChange={column =>