Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI Support for Store filtering #3166

Merged
merged 37 commits into from
Sep 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c435466
fetched lsit of stores in PanelList and passed into its content
aribalam Sep 14, 2020
cab4ad4
added store matches to panel options
aribalam Sep 14, 2020
4630a6f
added stores as props in panels
aribalam Sep 14, 2020
85024a6
display panel list only when stores are fetched
aribalam Sep 14, 2020
4b067f3
added UI for selecting stores
aribalam Sep 14, 2020
94d1ab2
created handler for store selection checkbozes
aribalam Sep 14, 2020
782efe8
added handlers to store checkboxes
aribalam Sep 14, 2020
dccb006
add storeMatches to URLParams for query
aribalam Sep 14, 2020
d8a4caa
fix filter bug
aribalam Sep 14, 2020
fde8af4
removed console logs
aribalam Sep 14, 2020
7b756d8
added label for store filter
aribalam Sep 14, 2020
bc9b4af
minor changes in code
aribalam Sep 16, 2020
7b7556b
added alert in case of error fetching stores list
aribalam Sep 16, 2020
0c18949
load panel list with status indicator till store list are fetched
aribalam Sep 16, 2020
d741a7d
removed lint errors
aribalam Sep 16, 2020
3658e84
added key prop for checkboxes
aribalam Sep 16, 2020
1d4c9c3
added react-select to yarn
aribalam Sep 17, 2020
8cbdd85
added react-select to package.json
aribalam Sep 17, 2020
20a4b5d
preprocess stores response data before passing as props to panels
aribalam Sep 17, 2020
08121ef
added dropdown ui for store selection
aribalam Sep 17, 2020
0d89425
removed redundant code
aribalam Sep 17, 2020
df62297
added style for label and wrapper
aribalam Sep 17, 2020
471db93
added support to encode and decode storeMatch from URI params
aribalam Sep 18, 2020
e4e9894
removed linting errors
aribalam Sep 18, 2020
063eff8
removed linting errors
aribalam Sep 18, 2020
9fa8532
select component takes initial values
aribalam Sep 18, 2020
5853968
updated bindata.go
aribalam Sep 18, 2020
62a5931
added store props and options in panels
aribalam Sep 20, 2020
126bac5
removed lint errors
aribalam Sep 20, 2020
f5125c8
added PR to changelog
aribalam Sep 21, 2020
bbf2969
handle null case for selected stores
aribalam Sep 21, 2020
1d12439
added checkbox for debug mode
aribalam Sep 23, 2020
9e92d7e
clear selected store on debug mode off
aribalam Sep 23, 2020
ba86337
changed checkbox name and corrected comment styles
aribalam Sep 24, 2020
06bfef5
removed ternary checking
aribalam Sep 24, 2020
3adef70
removed lint errors
aribalam Sep 24, 2020
02534b9
make assets
aribalam Sep 24, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ We use *breaking :warning:* to mark changes that are not backward compatible (re

### Added

- [#3166](https://github.com/thanos-io/thanos/pull/3166) UIs: Added UI for passing a `storeMatch[]` parameter to queries.
- [#3184](https://github.com/thanos-io/thanos/pull/3184) Compact: Fix web.prefix-header to use &wc.prefixHeaderName
- [#3181](https://github.com/thanos-io/thanos/pull/3181) Logging: Add debug level logging for responses between 300-399
- [#3133](https://github.com/thanos-io/thanos/pull/3133) Query: Allow passing a `storeMatch[]` to Labels APIs. Also time range metadata based store filtering is supported on Labels APIs.
Expand Down
274 changes: 137 additions & 137 deletions pkg/ui/bindata.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pkg/ui/react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-dom": "^16.8.0",
"@types/react-resize-detector": "^4.0.2",
"@types/react-select": "^3.0.20",
"@types/sanitize-html": "^1.20.2",
"bootstrap": "^4.2.1",
"css.escape": "^1.5.1",
Expand All @@ -37,6 +38,7 @@
"react-dom": "^16.7.0",
"react-resize-detector": "^4.2.1",
"react-scripts": "^3.4.0",
"react-select": "^3.1.0",
"react-test-renderer": "^16.9.0",
"reactstrap": "^8.0.1",
"sanitize-html": "^1.20.1",
Expand Down
9 changes: 9 additions & 0 deletions pkg/ui/react-app/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,12 @@ button.execute-btn {
display: block;
font-family: monospace;
}

.store-filter-wrapper {
display: flex;
}

.store-filter-label {
width: 100px;
padding: 5px;
}
3 changes: 3 additions & 0 deletions pkg/ui/react-app/src/pages/graph/Panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const defaultProps = {
maxSourceResolution: 'auto',
useDeduplication: true,
usePartialResponse: false,
storeMatches: [],
},
onOptionsChanged: (): void => {
// Do nothing.
Expand All @@ -37,6 +38,7 @@ const defaultProps = {
onExecuteQuery: (): void => {
// Do nothing.
},
stores: [],
};

describe('Panel', () => {
Expand Down Expand Up @@ -96,6 +98,7 @@ describe('Panel', () => {
maxSourceResolution: 'auto',
useDeduplication: true,
usePartialResponse: false,
storeMatches: [],
};
const graphPanel = mount(<Panel {...defaultProps} options={options} />);
const controls = graphPanel.find(GraphControls);
Expand Down
38 changes: 37 additions & 1 deletion pkg/ui/react-app/src/pages/graph/Panel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { Component } from 'react';

import { UncontrolledAlert, Button, Col, Nav, NavItem, NavLink, Row, TabContent, TabPane } from 'reactstrap';
import Select from 'react-select';

import moment from 'moment-timezone';

Expand All @@ -11,6 +12,7 @@ import { GraphTabContent } from './GraphTabContent';
import DataTable from './DataTable';
import TimeInput from './TimeInput';
import QueryStatsView, { QueryStats } from './QueryStatsView';
import { Store } from '../../thanos/pages/stores/store';
import PathPrefixProps from '../../types/PathPrefixProps';
import { QueryParams } from '../../types/types';

Expand All @@ -23,6 +25,7 @@ interface PanelProps {
metricNames: string[];
removePanel: () => void;
onExecuteQuery: (query: string) => void;
stores: Store[];
}

interface PanelState {
Expand All @@ -44,6 +47,7 @@ export interface PanelOptions {
maxSourceResolution: string;
useDeduplication: boolean;
usePartialResponse: boolean;
storeMatches: Store[];
}

export enum PanelType {
Expand All @@ -61,6 +65,7 @@ export const PanelDefaultOptions: PanelOptions = {
maxSourceResolution: '0s',
useDeduplication: true,
usePartialResponse: false,
storeMatches: [],
};

class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
Expand All @@ -80,6 +85,7 @@ class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {

this.handleChangeDeduplication = this.handleChangeDeduplication.bind(this);
this.handleChangePartialResponse = this.handleChangePartialResponse.bind(this);
this.handleStoreMatchChange = this.handleStoreMatchChange.bind(this);
}

componentDidUpdate({ options: prevOpts }: PanelProps) {
Expand All @@ -91,6 +97,7 @@ class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
maxSourceResolution,
useDeduplication,
usePartialResponse,
// TODO: Add support for Store Matches
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd nice to see this get done in this PR, but it's not a blocker :)

} = this.props.options;
if (
prevOpts.endTime !== endTime ||
Expand All @@ -100,6 +107,7 @@ class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
prevOpts.maxSourceResolution !== maxSourceResolution ||
prevOpts.useDeduplication !== useDeduplication ||
prevOpts.usePartialResponse !== usePartialResponse
// Check store matches
) {
this.executeQuery();
}
Expand Down Expand Up @@ -138,6 +146,11 @@ class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
partial_response: this.props.options.usePartialResponse.toString(),
});

// Add storeMatches to query params.
this.props.options.storeMatches?.forEach((store: Store) =>
params.append('storeMatch[]', `{__address__="${store.name}"}`)
);

let path: string;
switch (this.props.options.type) {
case 'graph':
Expand Down Expand Up @@ -255,8 +268,12 @@ class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
this.setOptions({ usePartialResponse: event.target.checked });
};

handleStoreMatchChange = (selectedStores: any): void => {
this.setOptions({ storeMatches: selectedStores || [] });
};

render() {
const { pastQueries, metricNames, options, id } = this.props;
const { pastQueries, metricNames, options, id, stores } = this.props;
return (
<div className="panel">
<Row>
Expand Down Expand Up @@ -296,6 +313,25 @@ class Panel extends Component<PanelProps & PathPrefixProps, PanelState> {
</Checkbox>
</Col>
</Row>
{stores?.length > 0 && (
<Row>
<Col>
<div className="store-filter-wrapper">
<label className="store-filter-label">Store Filter:</label>
<Select
defaultValue={options.storeMatches}
options={stores}
isMulti
getOptionLabel={(option: Store) => option.name}
getOptionValue={(option: Store) => option.name}
closeMenuOnSelect={false}
styles={{ container: (provided, state) => ({ ...provided, marginBottom: 20, zIndex: 3, width: '100%' }) }}
onChange={this.handleStoreMatchChange}
/>
</div>
</Col>
</Row>
)}
<Row>
<Col>
<Nav tabs>
Expand Down
41 changes: 39 additions & 2 deletions pkg/ui/react-app/src/pages/graph/PanelList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { UncontrolledAlert, Button } from 'reactstrap';
import Panel, { PanelOptions, PanelDefaultOptions } from './Panel';
import Checkbox from '../../components/Checkbox';
import PathPrefixProps from '../../types/PathPrefixProps';
import { StoreListProps } from '../../thanos/pages/stores/Stores';
import { Store } from '../../thanos/pages/stores/store';
import { generateID, decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString, callAll } from '../../utils';
import { useFetch } from '../../hooks/useFetch';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { withStatusIndicator } from '../../components/withStatusIndicator';

export type PanelMeta = { key: string; options: PanelOptions; id: string };

Expand All @@ -21,19 +24,30 @@ interface PanelListProps extends PathPrefixProps, RouteComponentProps {
metrics: string[];
useLocalTime: boolean;
queryHistoryEnabled: boolean;
stores: StoreListProps;
}

export const PanelListContent: FC<PanelListProps> = ({
metrics = [],
useLocalTime,
pathPrefix,
queryHistoryEnabled,
stores = {},
...rest
}) => {
const [panels, setPanels] = useState(rest.panels);
const [historyItems, setLocalStorageHistoryItems] = useLocalStorage<string[]>('history', []);
const [storeData, setStoreData] = useState([] as Store[]);

useEffect(() => {
// Convert stores data to a unified stores array.
let storeList: Store[] = [];
for (let type in stores) {
storeList.push(...stores[type]);
}
setStoreData(storeList);
// Clear selected stores for each panel.
panels.forEach((panel: PanelMeta) => (panel.options.storeMatches = []));
!panels.length && addPanel();
window.onpopstate = () => {
const panels = decodePanelOptionsFromQueryString(window.location.search);
Expand All @@ -43,7 +57,7 @@ export const PanelListContent: FC<PanelListProps> = ({
};
// We want useEffect to act only as componentDidMount, but react still complains about the empty dependencies list.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [stores]);

const handleExecuteQuery = (query: string) => {
const isSimpleMetric = metrics.indexOf(query) !== -1;
Expand Down Expand Up @@ -99,6 +113,7 @@ export const PanelListContent: FC<PanelListProps> = ({
metricNames={metrics}
pastQueries={queryHistoryEnabled ? historyItems : []}
pathPrefix={pathPrefix}
stores={storeData}
/>
))}
<Button className="mb-3" color="primary" onClick={addPanel}>
Expand All @@ -108,12 +123,18 @@ export const PanelListContent: FC<PanelListProps> = ({
);
};

const PanelListContentWithIndicator = withStatusIndicator(PanelListContent);

const PanelList: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = '' }) => {
const [delta, setDelta] = useState(0);
const [useLocalTime, setUseLocalTime] = useLocalStorage('use-local-time', false);
const [enableQueryHistory, setEnableQueryHistory] = useLocalStorage('enable-query-history', false);
const [debugMode, setDebugMode] = useState(false);

const { response: metricsRes, error: metricsErr } = useFetch<string[]>(`${pathPrefix}/api/v1/label/__name__/values`);
const { response: storesRes, error: storesErr, isLoading: storesLoading } = useFetch<StoreListProps>(
`${pathPrefix}/api/v1/stores`
);

const browserTime = new Date().getTime() / 1000;
const { response: timeRes, error: timeErr } = useFetch<{ result: number[] }>(`${pathPrefix}/api/v1/query?query=time()`);
Expand Down Expand Up @@ -149,6 +170,14 @@ const PanelList: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = ''
>
Use local time
</Checkbox>
<Checkbox
wrapperStyles={{ marginLeft: 20, display: 'inline-block' }}
id="debug-mode-checkbox"
defaultChecked={debugMode}
onChange={({ target }) => setDebugMode(target.checked)}
>
Enable Store Filtering
</Checkbox>
{(delta > 30 || timeErr) && (
<UncontrolledAlert color="danger">
<strong>Warning: </strong>
Expand All @@ -163,12 +192,20 @@ const PanelList: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix = ''
Error fetching metrics list: Unexpected response status when fetching metric names: {metricsErr.message}
</UncontrolledAlert>
)}
<PanelListContent
{storesErr && (
<UncontrolledAlert color="danger">
<strong>Warning: </strong>
Error fetching stores list: Unexpected response status when fetching stores: {storesErr.message}
</UncontrolledAlert>
)}
<PanelListContentWithIndicator
panels={decodePanelOptionsFromQueryString(window.location.search)}
pathPrefix={pathPrefix}
useLocalTime={useLocalTime}
metrics={metricsRes.data}
stores={debugMode ? storesRes.data : {}}
queryHistoryEnabled={enableQueryHistory}
isLoading={storesLoading}
/>
</>
);
Expand Down
5 changes: 5 additions & 0 deletions pkg/ui/react-app/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ export const parseOption = (param: string): Partial<PanelOptions> => {

case 'partial_response':
return { usePartialResponse: decodedValue === '1' };

case 'store_matches':
return { storeMatches: JSON.parse(decodedValue) };
}
return {};
};
Expand All @@ -201,6 +204,7 @@ export const toQueryString = ({ key, options }: PanelMeta) => {
maxSourceResolution,
useDeduplication,
usePartialResponse,
storeMatches,
} = options;
const time = isPresent(endTime) ? formatTime(endTime) : false;
const urlParams = [
Expand All @@ -211,6 +215,7 @@ export const toQueryString = ({ key, options }: PanelMeta) => {
formatWithKey('max_source_resolution', maxSourceResolution),
formatWithKey('deduplicate', useDeduplication ? 1 : 0),
formatWithKey('partial_response', usePartialResponse ? 1 : 0),
formatWithKey('store_matches', JSON.stringify(storeMatches)),
time ? `${formatWithKey('end_input', time)}&${formatWithKey('moment_input', time)}` : '',
isPresent(resolution) ? formatWithKey('step_input', resolution) : '',
];
Expand Down
34 changes: 32 additions & 2 deletions pkg/ui/react-app/src/utils/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,26 @@ describe('Utils', () => {
});

describe('URL Params', () => {
const stores: any = [
{
name: 'thanos_sidecar_one:10901',
lastCheck: '2020-09-20T11:35:18.250713478Z',
lastError: null,
labelSets: [
{
labels: [
{
name: 'monitor',
value: 'prometheus_one',
},
],
},
],
minTime: 1600598100000,
maxTime: 9223372036854776000,
},
];

const panels: any = [
{
key: '0',
Expand All @@ -147,6 +167,7 @@ describe('Utils', () => {
useDeduplication: true,
usePartialResponse: false,
type: PanelType.Graph,
storeMatches: [],
},
},
{
Expand All @@ -161,11 +182,12 @@ describe('Utils', () => {
useDeduplication: false,
usePartialResponse: true,
type: PanelType.Table,
storeMatches: stores,
},
},
];
const query =
'?g0.expr=rate(node_cpu_seconds_total%7Bmode%3D%22system%22%7D%5B1m%5D)&g0.tab=0&g0.stacked=0&g0.range_input=1h&g0.max_source_resolution=raw&g0.deduplicate=1&g0.partial_response=0&g0.end_input=2019-10-25%2023%3A37%3A00&g0.moment_input=2019-10-25%2023%3A37%3A00&g1.expr=node_filesystem_avail_bytes&g1.tab=1&g1.stacked=0&g1.range_input=1h&g1.max_source_resolution=auto&g1.deduplicate=0&g1.partial_response=1';
'?g0.expr=rate(node_cpu_seconds_total%7Bmode%3D%22system%22%7D%5B1m%5D)&g0.tab=0&g0.stacked=0&g0.range_input=1h&g0.max_source_resolution=raw&g0.deduplicate=1&g0.partial_response=0&g0.store_matches=%5B%5D&g0.end_input=2019-10-25%2023%3A37%3A00&g0.moment_input=2019-10-25%2023%3A37%3A00&g1.expr=node_filesystem_avail_bytes&g1.tab=1&g1.stacked=0&g1.range_input=1h&g1.max_source_resolution=auto&g1.deduplicate=0&g1.partial_response=1&g1.store_matches=%5B%7B%22name%22%3A%22thanos_sidecar_one%3A10901%22%2C%22lastCheck%22%3A%222020-09-20T11%3A35%3A18.250713478Z%22%2C%22lastError%22%3Anull%2C%22labelSets%22%3A%5B%7B%22labels%22%3A%5B%7B%22name%22%3A%22monitor%22%2C%22value%22%3A%22prometheus_one%22%7D%5D%7D%5D%2C%22minTime%22%3A1600598100000%2C%22maxTime%22%3A9223372036854776000%7D%5D';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should only store the name of the store in the URI as the name is sufficient to identify a store uniquely.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I too thought of passing only the store name, but the Select component that is being used takes in only objects as options. So passing a string array was not an option. I can create an interface with only store name as the key though. Would like your opinion on this.


describe('decodePanelOptionsFromQueryString', () => {
it('returns [] when query is empty', () => {
Expand Down Expand Up @@ -204,6 +226,13 @@ describe('Utils', () => {
it('should parse partial_response', () => {
expect(parseOption('partial_response=1')).toEqual({ usePartialResponse: true });
});
it('it should parse store_matches', () => {
expect(
parseOption(
'store_matches=%5B%7B%22name%22%3A%22thanos_sidecar_one%3A10901%22%2C%22lastCheck%22%3A%222020-09-20T11%3A35%3A18.250713478Z%22%2C%22lastError%22%3Anull%2C%22labelSets%22%3A%5B%7B%22labels%22%3A%5B%7B%22name%22%3A%22monitor%22%2C%22value%22%3A%22prometheus_one%22%7D%5D%7D%5D%2C%22minTime%22%3A1600598100000%2C%22maxTime%22%3A9223372036854776000%7D%5D'
)
).toEqual({ storeMatches: stores });
});

describe('step_input', () => {
it('should return step_input parsed if > 0', () => {
Expand Down Expand Up @@ -249,10 +278,11 @@ describe('Utils', () => {
maxSourceResolution: 'raw',
useDeduplication: true,
usePartialResponse: false,
storeMatches: [],
},
})
).toEqual(
'g0.expr=foo&g0.tab=0&g0.stacked=1&g0.range_input=0y&g0.max_source_resolution=raw&g0.deduplicate=1&g0.partial_response=0&g0.step_input=1'
'g0.expr=foo&g0.tab=0&g0.stacked=1&g0.range_input=0y&g0.max_source_resolution=raw&g0.deduplicate=1&g0.partial_response=0&g0.store_matches=%5B%5D&g0.step_input=1'
);
});
});
Expand Down
Loading