From f419fba40ecd57eb331376e8b6155ba3ba1e6669 Mon Sep 17 00:00:00 2001 From: Boyko Lalov Date: Fri, 15 Nov 2019 18:49:30 +0300 Subject: [PATCH] change the dynamic key with static id for react key prop Signed-off-by: blalov --- web/ui/react-app/src/pages/PanelList.tsx | 108 ++++++++--------- web/ui/react-app/src/utils/func.ts | 5 + web/ui/react-app/src/utils/urlParams.ts | 143 +++++++++-------------- 3 files changed, 113 insertions(+), 143 deletions(-) create mode 100644 web/ui/react-app/src/utils/func.ts diff --git a/web/ui/react-app/src/pages/PanelList.tsx b/web/ui/react-app/src/pages/PanelList.tsx index 347866e85e..f3553123ab 100644 --- a/web/ui/react-app/src/pages/PanelList.tsx +++ b/web/ui/react-app/src/pages/PanelList.tsx @@ -7,42 +7,39 @@ 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; timeDriftError: string | null; } +const initialPanel = { + id: generateID(), + key: '0', + options: PanelDefaultOptions, +}; + class PanelList extends Component { - private key = 0; constructor(props: PathPrefixProps) { super(props); const urlPanels = decodePanelOptionsFromQueryString(window.location.search); this.state = { - panels: - urlPanels.length !== 0 - ? urlPanels - : [ - { - key: this.getKey(), - options: PanelDefaultOptions, - }, - ], + panels: urlPanels.length ? urlPanels : [initialPanel], pastQueries: [], metricNames: [], fetchMetricsError: null, timeDriftError: null, }; + !urlPanels.length && this.updateURL(); } componentDidMount() { @@ -84,8 +81,8 @@ class PanelList extends Component { const panels = decodePanelOptionsFromQueryString(window.location.search); - if (panels.length !== 0) { - this.setState({ panels: panels }); + if (panels.length) { + this.setState({ panels }); } }; @@ -123,46 +120,43 @@ class PanelList extends Component { - if (key === p.key) { - return { - key: key, - options: opts, - }; - } - return p; + handleOptionsChanged = (key: string, options: PanelOptions) => { + const panels = this.state.panels.map(panel => { + return key === panel.key + ? { + ...panel, + options, + } + : panel; }); - this.setState({ panels: newPanels }, this.updateURL); - } - - updateURL(): void { - 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); + this.setState({ panels }, this.updateURL); }; - removePanel = (key: string): void => { - const panels = this.state.panels.filter(panel => { - return panel.key !== key; - }); - this.setState({ panels: panels }, this.updateURL); + updateURL() { + const query = encodePanelOptionsToQueryString(this.state.panels); + window.history.pushState({}, '', query); + }; + + addPanel = () => { + const { panels } = this.state; + const addedPanel = { + id: generateID(), + key: `${panels.length}`, + options: PanelDefaultOptions, + }; + this.setState({ panels: [...panels, addedPanel] }, this.updateURL); + }; + + removePanel = (key: string) => { + let newKey = 0; + const panels = this.state.panels.reduce((acc, panel) => { + return panel.key !== key ? [...acc, { ...panel, key: `${newKey++}` }] : acc; + }, []); + this.setState({ panels }, 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 +174,7 @@ class PanelList extends Component {timeDriftError && ( - Warning: Error fetching server time: {this.state.timeDriftError} + Warning: Error fetching server time: {timeDriftError} )} @@ -189,18 +183,18 @@ class PanelList extends Component {fetchMetricsError && ( - Warning: Error fetching metrics list: {this.state.fetchMetricsError} + Warning: Error fetching metrics list: {fetchMetricsError} )} - {this.state.panels.map(p => ( + {panels.map(({ id, options, key }) => ( this.handleOptionsChanged(p.key, opts)} - removePanel={() => this.removePanel(p.key)} + key={id} + options={options} + onOptionsChanged={opts => this.handleOptionsChanged(key, opts)} + removePanel={() => this.removePanel(key)} metricNames={metricNames} pastQueries={pastQueries} pathPrefix={pathPrefix} diff --git a/web/ui/react-app/src/utils/func.ts b/web/ui/react-app/src/utils/func.ts new file mode 100644 index 0000000000..d711bc84df --- /dev/null +++ b/web/ui/react-app/src/utils/func.ts @@ -0,0 +1,5 @@ +export const generateID = () => + '_' + + Math.random() + .toString(36) + .substr(2, 9); diff --git a/web/ui/react-app/src/utils/urlParams.ts b/web/ui/react-app/src/utils/urlParams.ts index 38cc62813e..74ea6e51e2 100644 --- a/web/ui/react-app/src/utils/urlParams.ts +++ b/web/ui/react-app/src/utils/urlParams.ts @@ -1,118 +1,89 @@ import { parseRange, parseTime, formatRange, formatTime } from './timeFormat'; import { PanelOptions, PanelType, PanelDefaultOptions } from '../Panel'; +import { generateID } from './func'; +import { PanelMeta } from '../pages/PanelList'; -export function decodePanelOptionsFromQueryString(query: string): { key: string; options: PanelOptions }[] { - if (query === '') { - return []; - } +export const decodePanelOptionsFromQueryString = (query: string): PanelMeta[] => { + return query === '' ? [] : parseParams(query.substring(1).split('&')); +}; - const params = query.substring(1).split('&'); - 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 byParamFormat = (p: string) => /^g\d+\..+=.+$/.test(p); +const parseParams = (params: string[]) => { let key = 0; - let options: IncompletePanelOptions = {}; - for (const p of sortedParams) { - const prefix = 'g' + key + '.'; + return params + .filter(byParamFormat) + .sort() + .reduce((panels, urlParam, i, sortedParams) => { + const prefix = `g${key}.`; - if (!p.startsWith(prefix)) { - panelOpts.push({ - key: key.toString(), - options: { ...PanelDefaultOptions, ...options }, - }); - options = {}; - key++; - } + if (urlParam.startsWith(`${prefix}expr=`)) { + let options: Partial = {}; - addParam(options, p.substring(prefix.length)); - } - panelOpts.push({ - key: key.toString(), - options: { ...PanelDefaultOptions, ...options }, - }); + 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 panelOpts; -} + return [ + ...panels, + { + id: generateID(), + key: `${key++}`, + options: { ...PanelDefaultOptions, ...options }, + }, + ]; + } -function addParam(opts: IncompletePanelOptions, param: string): void { + return panels; + }, []); +}; + +const parseOption = (param: string): Partial => { let [opt, val] = param.split('='); val = decodeURIComponent(val.replace(/\+/g, ' ')); - switch (opt) { case 'expr': - opts.expr = val; - break; + return { expr: val }; case 'tab': - if (val === '0') { - opts.type = PanelType.Graph; - } else { - opts.type = PanelType.Table; - } - break; + return { type: val === '0' ? PanelType.Graph : PanelType.Table }; case 'stacked': - opts.stacked = val === '1'; - break; + return { stacked: val === '1' }; case 'range_input': const range = parseRange(val); - if (range !== null) { - opts.range = range; - } - break; + return range ? { range } : {}; case 'end_input': - opts.endTime = parseTime(val); - break; + case 'moment_input': + return { endTime: parseTime(val) }; 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(val); + return resolution ? { resolution } : {}; } -} + return {}; +}; -export function encodePanelOptionsToQueryString(panels: { key: string; options: PanelOptions }[]): string { +export const encodePanelOptionsToQueryString = (panels: PanelMeta[]) => { const queryParams: string[] = []; - panels.forEach(p => { - const prefix = 'g' + p.key + '.'; - const o = p.options; + panels.forEach(({ key, options }) => { + const prefix = `g${key}.`; + const { expr, type, stacked, range, endTime, resolution } = 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, + expr, + tab: type === PanelType.Graph ? '0' : '1', + stacked: stacked ? '1' : '0', + range_input: formatRange(range), + end_input: endTime ? formatTime(endTime) : undefined, + moment_input: endTime ? formatTime(endTime) : undefined, + step_input: resolution ? resolution.toString() : undefined, }; for (const o in panelParams) { @@ -124,4 +95,4 @@ export function encodePanelOptionsToQueryString(panels: { key: string; options: }); return '?' + queryParams.join('&'); -} +};