mirror of https://github.com/apache/druid.git
Web console: refactor home view, add tests (#8247)
* refactor home view * updated mode button placement
This commit is contained in:
parent
38b6047aa9
commit
b9c68a5b7b
|
@ -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}';
|
||||
|
||||
|
|
|
@ -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"`;
|
|
@ -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', () => {
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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"
|
||||
<StatusCard />
|
||||
<DatasourcesCard
|
||||
noSqlMode={false}
|
||||
/>
|
||||
|
||||
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"
|
||||
<SegmentsCard
|
||||
noSqlMode={false}
|
||||
/>
|
||||
|
||||
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"
|
||||
<SupervisorsCard />
|
||||
<TasksCard
|
||||
noSqlMode={false}
|
||||
/>
|
||||
|
||||
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"
|
||||
<ServersCard
|
||||
noSqlMode={false}
|
||||
/>
|
||||
|
||||
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>
|
||||
<LookupsCard />
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
`;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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} />
|
||||
{title}
|
||||
</H5>
|
||||
{loading ? <p>Loading...</p> : error ? `Error: ${error}` : children}
|
||||
</Card>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -27,8 +27,4 @@
|
|||
text-decoration: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.home-view-card {
|
||||
height: 170px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 [
|
||||
|
|
|
@ -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) })}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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 =>
|
||||
|
|
Loading…
Reference in New Issue