change the dynamic key with static id for react key prop

Signed-off-by: blalov <boyko.lalov@tick42.com>
This commit is contained in:
Boyko Lalov 2019-11-15 18:49:30 +03:00 committed by blalov
parent 23c0299d85
commit f419fba40e
3 changed files with 113 additions and 143 deletions

View file

@ -7,42 +7,39 @@ import Panel, { PanelOptions, PanelDefaultOptions } from '../Panel';
import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString } from '../utils/urlParams'; import { decodePanelOptionsFromQueryString, encodePanelOptionsToQueryString } from '../utils/urlParams';
import Checkbox from '../Checkbox'; import Checkbox from '../Checkbox';
import PathPrefixProps from '../PathPrefixProps'; import PathPrefixProps from '../PathPrefixProps';
import { generateID } from '../utils/func';
export type MetricGroup = { title: string; items: string[] }; export type MetricGroup = { title: string; items: string[] };
export type PanelMeta = { key: string; options: PanelOptions; id: string };
interface PanelListState { interface PanelListState {
panels: { panels: PanelMeta[];
key: string;
options: PanelOptions;
}[];
pastQueries: string[]; pastQueries: string[];
metricNames: string[]; metricNames: string[];
fetchMetricsError: string | null; fetchMetricsError: string | null;
timeDriftError: string | null; timeDriftError: string | null;
} }
const initialPanel = {
id: generateID(),
key: '0',
options: PanelDefaultOptions,
};
class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelListState> { class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelListState> {
private key = 0;
constructor(props: PathPrefixProps) { constructor(props: PathPrefixProps) {
super(props); super(props);
const urlPanels = decodePanelOptionsFromQueryString(window.location.search); const urlPanels = decodePanelOptionsFromQueryString(window.location.search);
this.state = { this.state = {
panels: panels: urlPanels.length ? urlPanels : [initialPanel],
urlPanels.length !== 0
? urlPanels
: [
{
key: this.getKey(),
options: PanelDefaultOptions,
},
],
pastQueries: [], pastQueries: [],
metricNames: [], metricNames: [],
fetchMetricsError: null, fetchMetricsError: null,
timeDriftError: null, timeDriftError: null,
}; };
!urlPanels.length && this.updateURL();
} }
componentDidMount() { componentDidMount() {
@ -84,8 +81,8 @@ class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelLi
window.onpopstate = () => { window.onpopstate = () => {
const panels = decodePanelOptionsFromQueryString(window.location.search); const panels = decodePanelOptionsFromQueryString(window.location.search);
if (panels.length !== 0) { if (panels.length) {
this.setState({ panels: panels }); this.setState({ panels });
} }
}; };
@ -123,46 +120,43 @@ class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelLi
this.updatePastQueries(); this.updatePastQueries();
}; };
getKey(): string { handleOptionsChanged = (key: string, options: PanelOptions) => {
return (this.key++).toString(); const panels = this.state.panels.map(panel => {
return key === panel.key
? {
...panel,
options,
} }
: panel;
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); this.setState({ panels }, this.updateURL);
} };
updateURL(): void { updateURL() {
const query = encodePanelOptionsToQueryString(this.state.panels); const query = encodePanelOptionsToQueryString(this.state.panels);
window.history.pushState({}, '', query); 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);
}; };
removePanel = (key: string): void => { addPanel = () => {
const panels = this.state.panels.filter(panel => { const { panels } = this.state;
return panel.key !== key; const addedPanel = {
}); id: generateID(),
this.setState({ panels: panels }, this.updateURL); key: `${panels.length}`,
options: PanelDefaultOptions,
};
this.setState({ panels: [...panels, addedPanel] }, this.updateURL);
};
removePanel = (key: string) => {
let newKey = 0;
const panels = this.state.panels.reduce<PanelMeta[]>((acc, panel) => {
return panel.key !== key ? [...acc, { ...panel, key: `${newKey++}` }] : acc;
}, []);
this.setState({ panels }, this.updateURL);
}; };
render() { render() {
const { metricNames, pastQueries, timeDriftError, fetchMetricsError } = this.state; const { metricNames, pastQueries, timeDriftError, fetchMetricsError, panels } = this.state;
const { pathPrefix } = this.props; const { pathPrefix } = this.props;
return ( return (
<> <>
@ -180,7 +174,7 @@ class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelLi
<Col> <Col>
{timeDriftError && ( {timeDriftError && (
<Alert color="danger"> <Alert color="danger">
<strong>Warning:</strong> Error fetching server time: {this.state.timeDriftError} <strong>Warning:</strong> Error fetching server time: {timeDriftError}
</Alert> </Alert>
)} )}
</Col> </Col>
@ -189,18 +183,18 @@ class PanelList extends Component<RouteComponentProps & PathPrefixProps, PanelLi
<Col> <Col>
{fetchMetricsError && ( {fetchMetricsError && (
<Alert color="danger"> <Alert color="danger">
<strong>Warning:</strong> Error fetching metrics list: {this.state.fetchMetricsError} <strong>Warning:</strong> Error fetching metrics list: {fetchMetricsError}
</Alert> </Alert>
)} )}
</Col> </Col>
</Row> </Row>
{this.state.panels.map(p => ( {panels.map(({ id, options, key }) => (
<Panel <Panel
onExecuteQuery={this.handleQueryHistory} onExecuteQuery={this.handleQueryHistory}
key={p.key} key={id}
options={p.options} options={options}
onOptionsChanged={(opts: PanelOptions) => this.handleOptionsChanged(p.key, opts)} onOptionsChanged={opts => this.handleOptionsChanged(key, opts)}
removePanel={() => this.removePanel(p.key)} removePanel={() => this.removePanel(key)}
metricNames={metricNames} metricNames={metricNames}
pastQueries={pastQueries} pastQueries={pastQueries}
pathPrefix={pathPrefix} pathPrefix={pathPrefix}

View file

@ -0,0 +1,5 @@
export const generateID = () =>
'_' +
Math.random()
.toString(36)
.substr(2, 9);

View file

@ -1,118 +1,89 @@
import { parseRange, parseTime, formatRange, formatTime } from './timeFormat'; import { parseRange, parseTime, formatRange, formatTime } from './timeFormat';
import { PanelOptions, PanelType, PanelDefaultOptions } from '../Panel'; import { PanelOptions, PanelType, PanelDefaultOptions } from '../Panel';
import { generateID } from './func';
import { PanelMeta } from '../pages/PanelList';
export function decodePanelOptionsFromQueryString(query: string): { key: string; options: PanelOptions }[] { export const decodePanelOptionsFromQueryString = (query: string): PanelMeta[] => {
if (query === '') { return query === '' ? [] : parseParams(query.substring(1).split('&'));
return []; };
}
const params = query.substring(1).split('&'); const byParamFormat = (p: string) => /^g\d+\..+=.+$/.test(p);
return parseParams(params);
}
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 }[] = [];
const parseParams = (params: string[]) => {
let key = 0; let key = 0;
let options: IncompletePanelOptions = {}; return params
for (const p of sortedParams) { .filter(byParamFormat)
const prefix = 'g' + key + '.'; .sort()
.reduce<PanelMeta[]>((panels, urlParam, i, sortedParams) => {
const prefix = `g${key}.`;
if (!p.startsWith(prefix)) { if (urlParam.startsWith(`${prefix}expr=`)) {
panelOpts.push({ let options: Partial<PanelOptions> = {};
key: key.toString(),
for (let index = i; index < sortedParams.length; index++) {
const param = sortedParams[index];
if (!param.startsWith(prefix)) {
break;
}
options = { ...options, ...parseOption(param.substring(prefix.length)) };
}
return [
...panels,
{
id: generateID(),
key: `${key++}`,
options: { ...PanelDefaultOptions, ...options }, options: { ...PanelDefaultOptions, ...options },
}); },
options = {}; ];
key++;
} }
addParam(options, p.substring(prefix.length)); return panels;
} }, []);
panelOpts.push({ };
key: key.toString(),
options: { ...PanelDefaultOptions, ...options },
});
return panelOpts; const parseOption = (param: string): Partial<PanelOptions> => {
}
function addParam(opts: IncompletePanelOptions, param: string): void {
let [opt, val] = param.split('='); let [opt, val] = param.split('=');
val = decodeURIComponent(val.replace(/\+/g, ' ')); val = decodeURIComponent(val.replace(/\+/g, ' '));
switch (opt) { switch (opt) {
case 'expr': case 'expr':
opts.expr = val; return { expr: val };
break;
case 'tab': case 'tab':
if (val === '0') { return { type: val === '0' ? PanelType.Graph : PanelType.Table };
opts.type = PanelType.Graph;
} else {
opts.type = PanelType.Table;
}
break;
case 'stacked': case 'stacked':
opts.stacked = val === '1'; return { stacked: val === '1' };
break;
case 'range_input': case 'range_input':
const range = parseRange(val); const range = parseRange(val);
if (range !== null) { return range ? { range } : {};
opts.range = range;
}
break;
case 'end_input': case 'end_input':
opts.endTime = parseTime(val); case 'moment_input':
break; return { endTime: parseTime(val) };
case 'step_input': case 'step_input':
const res = parseInt(val); const resolution = parseInt(val);
if (res > 0) { return resolution ? { resolution } : {};
opts.resolution = res;
} }
break; return {};
};
case 'moment_input': export const encodePanelOptionsToQueryString = (panels: PanelMeta[]) => {
opts.endTime = parseTime(val);
break;
}
}
export function encodePanelOptionsToQueryString(panels: { key: string; options: PanelOptions }[]): string {
const queryParams: string[] = []; const queryParams: string[] = [];
panels.forEach(p => { panels.forEach(({ key, options }) => {
const prefix = 'g' + p.key + '.'; const prefix = `g${key}.`;
const o = p.options; const { expr, type, stacked, range, endTime, resolution } = options;
const panelParams: { [key: string]: string | undefined } = { const panelParams: { [key: string]: string | undefined } = {
expr: o.expr, expr,
tab: o.type === PanelType.Graph ? '0' : '1', tab: type === PanelType.Graph ? '0' : '1',
stacked: o.stacked ? '1' : '0', stacked: stacked ? '1' : '0',
range_input: formatRange(o.range), range_input: formatRange(range),
end_input: o.endTime !== null ? formatTime(o.endTime) : undefined, end_input: endTime ? formatTime(endTime) : undefined,
moment_input: o.endTime !== null ? formatTime(o.endTime) : undefined, moment_input: endTime ? formatTime(endTime) : undefined,
step_input: o.resolution !== null ? o.resolution.toString() : undefined, step_input: resolution ? resolution.toString() : undefined,
}; };
for (const o in panelParams) { for (const o in panelParams) {
@ -124,4 +95,4 @@ export function encodePanelOptionsToQueryString(panels: { key: string; options:
}); });
return '?' + queryParams.join('&'); return '?' + queryParams.join('&');
} };