mirror of
https://github.com/prometheus/prometheus.git
synced 2024-12-25 21:54:10 -08:00
React UI: Change the dynamic react key with static id (#6325)
* change the dynamic key with static id for react key prop Signed-off-by: blalov <boyko.lalov@tick42.com> * test fix Signed-off-by: blalov <boyko.lalov@tick42.com> * add initial panel on did mount Signed-off-by: blalov <boyko.lalov@tick42.com> * lint fix Signed-off-by: blalov <boyko.lalov@tick42.com> * remove filter for empty expression Signed-off-by: blalov <boyko.lalov@tick42.com> * refactoring Signed-off-by: blalov <boyko.lalov@tick42.com> * refactor encodePanelOptionsToQueryString Signed-off-by: blalov <boyko.lalov@tick42.com> * refactor encodePanelOptionsToQueryString Signed-off-by: blalov <boyko.lalov@tick42.com> * revert to format range/endTime Signed-off-by: blalov <boyko.lalov@tick42.com> * urlParams unit tests Signed-off-by: blalov <boyko.lalov@tick42.com> * unit tests adjustments Signed-off-by: Boyko Lalov <boyskila@gmail.com>
This commit is contained in:
parent
2a190d1168
commit
3e3b7ceb0b
|
@ -7,14 +7,13 @@ import Panel, { PanelOptions, PanelDefaultOptions } from '../Panel';
|
|||
import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString } from '../utils/urlParams';
|
||||
import Checkbox from '../Checkbox';
|
||||
import PathPrefixProps from '../PathPrefixProps';
|
||||
import { generateID } from '../utils/func';
|
||||
|
||||
export type MetricGroup = { title: string; items: string[] };
|
||||
export type PanelMeta = { key: string; options: PanelOptions; id: string };
|
||||
|
||||
interface PanelListState {
|
||||
panels: {
|
||||
key: string;
|
||||
options: PanelOptions;
|
||||
}[];
|
||||
panels: PanelMeta[];
|
||||
pastQueries: string[];
|
||||
metricNames: string[];
|
||||
fetchMetricsError: string | null;
|
||||
|
@ -22,22 +21,11 @@ interface PanelListState {
|
|||
}
|
||||
|
||||
class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelListState> {
|
||||
private key = 0;
|
||||
constructor(props: PathPrefixProps) {
|
||||
constructor(props: RouteComponentProps & PathPrefixProps) {
|
||||
super(props);
|
||||
|
||||
const urlPanels = decodePanelOptionsFromQueryString(window.location.search);
|
||||
|
||||
this.state = {
|
||||
panels:
|
||||
urlPanels.length !== 0
|
||||
? urlPanels
|
||||
: [
|
||||
{
|
||||
key: this.getKey(),
|
||||
options: PanelDefaultOptions,
|
||||
},
|
||||
],
|
||||
panels: decodePanelOptionsFromQueryString(window.location.search),
|
||||
pastQueries: [],
|
||||
metricNames: [],
|
||||
fetchMetricsError: null,
|
||||
|
@ -46,6 +34,7 @@ class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelLi
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
!this.state.panels.length && this.addPanel();
|
||||
fetch(`${this.props.pathPrefix}/api/v1/label/__name__/values`, { cache: 'no-store' })
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
|
@ -84,8 +73,8 @@ class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelLi
|
|||
|
||||
window.onpopstate = () => {
|
||||
const panels = decodePanelOptionsFromQueryString(window.location.search);
|
||||
if (panels.length !== 0) {
|
||||
this.setState({ panels: panels });
|
||||
if (panels.length > 0) {
|
||||
this.setState({ panels });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -123,46 +112,42 @@ class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelLi
|
|||
this.updatePastQueries();
|
||||
};
|
||||
|
||||
getKey(): string {
|
||||
return (this.key++).toString();
|
||||
}
|
||||
|
||||
handleOptionsChanged(key: string, opts: PanelOptions): void {
|
||||
const newPanels = this.state.panels.map(p => {
|
||||
if (key === p.key) {
|
||||
return {
|
||||
key: key,
|
||||
options: opts,
|
||||
};
|
||||
}
|
||||
return p;
|
||||
});
|
||||
this.setState({ panels: newPanels }, this.updateURL);
|
||||
}
|
||||
|
||||
updateURL(): void {
|
||||
updateURL() {
|
||||
const query = encodePanelOptionsToQueryString(this.state.panels);
|
||||
window.history.pushState({}, '', query);
|
||||
}
|
||||
|
||||
addPanel = (): void => {
|
||||
const panels = this.state.panels.slice();
|
||||
panels.push({
|
||||
key: this.getKey(),
|
||||
options: PanelDefaultOptions,
|
||||
});
|
||||
this.setState({ panels: panels }, this.updateURL);
|
||||
handleOptionsChanged = (id: string, options: PanelOptions) => {
|
||||
const updatedPanels = this.state.panels.map(p => (id === p.id ? { ...p, options } : p));
|
||||
this.setState({ panels: updatedPanels }, this.updateURL);
|
||||
};
|
||||
|
||||
removePanel = (key: string): void => {
|
||||
const panels = this.state.panels.filter(panel => {
|
||||
return panel.key !== key;
|
||||
});
|
||||
this.setState({ panels: panels }, this.updateURL);
|
||||
addPanel = () => {
|
||||
const { panels } = this.state;
|
||||
const nextPanels = [
|
||||
...panels,
|
||||
{
|
||||
id: generateID(),
|
||||
key: `${panels.length}`,
|
||||
options: PanelDefaultOptions,
|
||||
},
|
||||
];
|
||||
this.setState({ panels: nextPanels }, this.updateURL);
|
||||
};
|
||||
|
||||
removePanel = (id: string) => {
|
||||
this.setState(
|
||||
{
|
||||
panels: this.state.panels.reduce<PanelMeta[]>((acc, panel) => {
|
||||
return panel.id !== id ? [...acc, { ...panel, key: `${acc.length}` }] : acc;
|
||||
}, []),
|
||||
},
|
||||
this.updateURL
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { metricNames, pastQueries, timeDriftError, fetchMetricsError } = this.state;
|
||||
const { metricNames, pastQueries, timeDriftError, fetchMetricsError, panels } = this.state;
|
||||
const { pathPrefix } = this.props;
|
||||
return (
|
||||
<>
|
||||
|
@ -180,7 +165,7 @@ class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelLi
|
|||
<Col>
|
||||
{timeDriftError && (
|
||||
<Alert color="danger">
|
||||
<strong>Warning:</strong> Error fetching server time: {this.state.timeDriftError}
|
||||
<strong>Warning:</strong> Error fetching server time: {timeDriftError}
|
||||
</Alert>
|
||||
)}
|
||||
</Col>
|
||||
|
@ -189,18 +174,18 @@ class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelLi
|
|||
<Col>
|
||||
{fetchMetricsError && (
|
||||
<Alert color="danger">
|
||||
<strong>Warning:</strong> Error fetching metrics list: {this.state.fetchMetricsError}
|
||||
<strong>Warning:</strong> Error fetching metrics list: {fetchMetricsError}
|
||||
</Alert>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
{this.state.panels.map(p => (
|
||||
{panels.map(({ id, options }) => (
|
||||
<Panel
|
||||
onExecuteQuery={this.handleQueryHistory}
|
||||
key={p.key}
|
||||
options={p.options}
|
||||
onOptionsChanged={(opts: PanelOptions) => this.handleOptionsChanged(p.key, opts)}
|
||||
removePanel={() => this.removePanel(p.key)}
|
||||
key={id}
|
||||
options={options}
|
||||
onOptionsChanged={opts => this.handleOptionsChanged(id, opts)}
|
||||
removePanel={() => this.removePanel(id)}
|
||||
metricNames={metricNames}
|
||||
pastQueries={pastQueries}
|
||||
pathPrefix={pathPrefix}
|
||||
|
|
9
web/ui/react-app/src/utils/func.ts
Normal file
9
web/ui/react-app/src/utils/func.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export const generateID = () => {
|
||||
return `_${Math.random()
|
||||
.toString(36)
|
||||
.substr(2, 9)}`;
|
||||
};
|
||||
|
||||
export const byEmptyString = (p: string) => p.length > 0;
|
||||
|
||||
export const isPresent = <T>(obj: T): obj is NonNullable<T> => obj !== null && obj !== undefined;
|
|
@ -1,7 +1,8 @@
|
|||
import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString } from './urlParams';
|
||||
import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString, parseOption, toQueryString } from './urlParams';
|
||||
import { PanelType } from '../Panel';
|
||||
import moment from 'moment';
|
||||
|
||||
const panels = [
|
||||
const panels: any = [
|
||||
{
|
||||
key: '0',
|
||||
options: {
|
||||
|
@ -33,7 +34,61 @@ describe('decodePanelOptionsFromQueryString', () => {
|
|||
expect(decodePanelOptionsFromQueryString('')).toEqual([]);
|
||||
});
|
||||
it('returns and array of parsed params when query string is non-empty', () => {
|
||||
expect(decodePanelOptionsFromQueryString(query)).toEqual(panels);
|
||||
expect(decodePanelOptionsFromQueryString(query)).toMatchObject(panels);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseOption', () => {
|
||||
it('should return empty object for invalid param', () => {
|
||||
expect(parseOption('invalid_prop=foo')).toEqual({});
|
||||
});
|
||||
it('should parse expr param', () => {
|
||||
expect(parseOption('expr=foo')).toEqual({ expr: 'foo' });
|
||||
});
|
||||
it('should parse stacked', () => {
|
||||
expect(parseOption('stacked=1')).toEqual({ stacked: true });
|
||||
});
|
||||
it('should parse end_input', () => {
|
||||
expect(parseOption('end_input=2019-10-25%2023%3A37')).toEqual({ endTime: moment.utc('2019-10-25 23:37').valueOf() });
|
||||
});
|
||||
it('should parse moment_input', () => {
|
||||
expect(parseOption('moment_input=2019-10-25%2023%3A37')).toEqual({ endTime: moment.utc('2019-10-25 23:37').valueOf() });
|
||||
});
|
||||
describe('step_input', () => {
|
||||
it('should return step_input parsed if > 0', () => {
|
||||
expect(parseOption('step_input=2')).toEqual({ resolution: 2 });
|
||||
});
|
||||
it('should return empty object if step is equal 0', () => {
|
||||
expect(parseOption('step_input=0')).toEqual({});
|
||||
});
|
||||
});
|
||||
describe('range_input', () => {
|
||||
it('should return range parsed if its not null', () => {
|
||||
expect(parseOption('range_input=2h')).toEqual({ range: 7200 });
|
||||
});
|
||||
it('should return empty object for invalid value', () => {
|
||||
expect(parseOption('range_input=h')).toEqual({});
|
||||
});
|
||||
});
|
||||
describe('Parse type param', () => {
|
||||
it('should return panel type "graph" if tab=0', () => {
|
||||
expect(parseOption('tab=0')).toEqual({ type: PanelType.Graph });
|
||||
});
|
||||
it('should return panel type "table" if tab=1', () => {
|
||||
expect(parseOption('tab=1')).toEqual({ type: PanelType.Table });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toQueryString', () => {
|
||||
it('should generate query string from panel options', () => {
|
||||
expect(
|
||||
toQueryString({
|
||||
id: 'asdf',
|
||||
key: '0',
|
||||
options: { expr: 'foo', type: PanelType.Graph, stacked: true, range: 0, endTime: null, resolution: 1 },
|
||||
})
|
||||
).toEqual('g0.expr=foo&g0.tab=0&g0.stacked=1&g0.range_input=0y&g0.step_input=1');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,127 +1,85 @@
|
|||
import { parseRange, parseTime, formatRange, formatTime } from './timeFormat';
|
||||
import { PanelOptions, PanelType, PanelDefaultOptions } from '../Panel';
|
||||
|
||||
export function decodePanelOptionsFromQueryString(query: string): { key: string; options: PanelOptions }[] {
|
||||
if (query === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const params = query.substring(1).split('&');
|
||||
return parseParams(params);
|
||||
}
|
||||
import { generateID, byEmptyString, isPresent } from './func';
|
||||
import { PanelMeta } from '../pages/PanelList';
|
||||
|
||||
const paramFormat = /^g\d+\..+=.+$/;
|
||||
|
||||
interface IncompletePanelOptions {
|
||||
expr?: string;
|
||||
type?: PanelType;
|
||||
range?: number;
|
||||
endTime?: number | null;
|
||||
resolution?: number | null;
|
||||
stacked?: boolean;
|
||||
}
|
||||
|
||||
function parseParams(params: string[]): { key: string; options: PanelOptions }[] {
|
||||
const sortedParams = params
|
||||
.filter(p => {
|
||||
return paramFormat.test(p);
|
||||
})
|
||||
.sort();
|
||||
|
||||
const panelOpts: { key: string; options: PanelOptions }[] = [];
|
||||
|
||||
let key = 0;
|
||||
let options: IncompletePanelOptions = {};
|
||||
for (const p of sortedParams) {
|
||||
const prefix = 'g' + key + '.';
|
||||
|
||||
if (!p.startsWith(prefix)) {
|
||||
panelOpts.push({
|
||||
key: key.toString(),
|
||||
options: { ...PanelDefaultOptions, ...options },
|
||||
});
|
||||
options = {};
|
||||
key++;
|
||||
}
|
||||
|
||||
addParam(options, p.substring(prefix.length));
|
||||
export const decodePanelOptionsFromQueryString = (query: string): PanelMeta[] => {
|
||||
if (query === '') {
|
||||
return [];
|
||||
}
|
||||
panelOpts.push({
|
||||
key: key.toString(),
|
||||
options: { ...PanelDefaultOptions, ...options },
|
||||
});
|
||||
const urlParams = query.substring(1).split('&');
|
||||
|
||||
return panelOpts;
|
||||
}
|
||||
|
||||
function addParam(opts: IncompletePanelOptions, param: string): void {
|
||||
let [opt, val] = param.split('=');
|
||||
val = decodeURIComponent(val.replace(/\+/g, ' '));
|
||||
return urlParams.reduce<PanelMeta[]>((panels, urlParam, i) => {
|
||||
const panelsCount = panels.length;
|
||||
const prefix = `g${panelsCount}.`;
|
||||
if (urlParam.startsWith(`${prefix}expr=`)) {
|
||||
const prefixLen = prefix.length;
|
||||
return [
|
||||
...panels,
|
||||
{
|
||||
id: generateID(),
|
||||
key: `${panelsCount}`,
|
||||
options: urlParams.slice(i).reduce((opts, param) => {
|
||||
return param.startsWith(prefix) && paramFormat.test(param)
|
||||
? { ...opts, ...parseOption(param.substring(prefixLen)) }
|
||||
: opts;
|
||||
}, PanelDefaultOptions),
|
||||
},
|
||||
];
|
||||
}
|
||||
return panels;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const parseOption = (param: string): Partial<PanelOptions> => {
|
||||
const [opt, val] = param.split('=');
|
||||
const decodedValue = decodeURIComponent(val.replace(/\+/g, ' '));
|
||||
switch (opt) {
|
||||
case 'expr':
|
||||
opts.expr = val;
|
||||
break;
|
||||
return { expr: decodedValue };
|
||||
|
||||
case 'tab':
|
||||
if (val === '0') {
|
||||
opts.type = PanelType.Graph;
|
||||
} else {
|
||||
opts.type = PanelType.Table;
|
||||
}
|
||||
break;
|
||||
return { type: decodedValue === '0' ? PanelType.Graph : PanelType.Table };
|
||||
|
||||
case 'stacked':
|
||||
opts.stacked = val === '1';
|
||||
break;
|
||||
return { stacked: decodedValue === '1' };
|
||||
|
||||
case 'range_input':
|
||||
const range = parseRange(val);
|
||||
if (range !== null) {
|
||||
opts.range = range;
|
||||
}
|
||||
break;
|
||||
const range = parseRange(decodedValue);
|
||||
return isPresent(range) ? { range } : {};
|
||||
|
||||
case 'end_input':
|
||||
opts.endTime = parseTime(val);
|
||||
break;
|
||||
case 'moment_input':
|
||||
return { endTime: parseTime(decodedValue) };
|
||||
|
||||
case 'step_input':
|
||||
const res = parseInt(val);
|
||||
if (res > 0) {
|
||||
opts.resolution = res;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'moment_input':
|
||||
opts.endTime = parseTime(val);
|
||||
break;
|
||||
const resolution = parseInt(decodedValue);
|
||||
return resolution > 0 ? { resolution } : {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export function encodePanelOptionsToQueryString(panels: { key: string; options: PanelOptions }[]): string {
|
||||
const queryParams: string[] = [];
|
||||
export const formatParam = (key: string) => (paramName: string, value: number | string | boolean) => {
|
||||
return `g${key}.${paramName}=${encodeURIComponent(value)}`;
|
||||
};
|
||||
|
||||
panels.forEach(p => {
|
||||
const prefix = 'g' + p.key + '.';
|
||||
const o = p.options;
|
||||
const panelParams: { [key: string]: string | undefined } = {
|
||||
expr: o.expr,
|
||||
tab: o.type === PanelType.Graph ? '0' : '1',
|
||||
stacked: o.stacked ? '1' : '0',
|
||||
range_input: formatRange(o.range),
|
||||
end_input: o.endTime !== null ? formatTime(o.endTime) : undefined,
|
||||
moment_input: o.endTime !== null ? formatTime(o.endTime) : undefined,
|
||||
step_input: o.resolution !== null ? o.resolution.toString() : undefined,
|
||||
};
|
||||
export const toQueryString = ({ key, options }: PanelMeta) => {
|
||||
const formatWithKey = formatParam(key);
|
||||
const { expr, type, stacked, range, endTime, resolution } = options;
|
||||
const time = isPresent(endTime) ? formatTime(endTime) : false;
|
||||
const urlParams = [
|
||||
formatWithKey('expr', expr),
|
||||
formatWithKey('tab', type === PanelType.Graph ? 0 : 1),
|
||||
formatWithKey('stacked', stacked ? 1 : 0),
|
||||
formatWithKey('range_input', formatRange(range)),
|
||||
time ? `${formatWithKey('end_input', time)}&${formatWithKey('moment_input', time)}` : '',
|
||||
isPresent(resolution) ? formatWithKey('step_input', resolution) : '',
|
||||
];
|
||||
return urlParams.filter(byEmptyString).join('&');
|
||||
};
|
||||
|
||||
for (const o in panelParams) {
|
||||
const pp = panelParams[o];
|
||||
if (pp !== undefined) {
|
||||
queryParams.push(prefix + o + '=' + encodeURIComponent(pp));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return '?' + queryParams.join('&');
|
||||
}
|
||||
export const encodePanelOptionsToQueryString = (panels: PanelMeta[]) => {
|
||||
return `?${panels.map(toQueryString).join('&')}`;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue