analytics/assets/js/dashboard/stats/reports/list.js

324 lines
11 KiB
JavaScript
Raw Normal View History

Realtime dashboard improvements (#2445) * add a new realtime-update-timer module * hook to the new 'tick' event in ListReport for auto-updates This commit fixes the bug where all reports using the `ListReport` component did not auto-update in realtime mode. Those reports are: - Pages (Top / Entry / Exit) - Locations (Countries / Regions / Cities) - Devices (Screen Sizes / Browsers + versions / OS-s + versions) * fetch data for ListReports only when scrolled into view * refactor fetching data in ListReport * refer to one source of truth for utm tags * make the 'All' tab in Sources auto-update * make all UTM tabs in Sources auto-update * fetch UTM data only when scrolled into view * auto-update Referrers with the new timer * auto-update google search terms * auto-update Conversions * make countries map auto-update * auto-update visitor-graph and top stats with new timer * use new tick event for current visitors (in Historical) * remove the old timer class * update changelog * Visual improvements to automatic realtime updates (#2532) * minor consistency fix for text color in dark mode * use FlipMove in goal conversions report * use FlipMove in ListReports * set main graph and top stats loading state correctly * refactor isIntervalValid function * enforce intervals are valid when set and stored * remove duplicate data fetching on interval change Fetching new data is handled by the `fetchGraphData` callback in `updateInterval` * refactor updateMetric function * make it clearer why 'metric' can be a faulty value * extract 'query' and 'site' variables from 'this.props' * reset interval state only when period is changed The 'maybeRollbackInterval' function was also used to fetch data. This commit replaces all those function calls with 'fetchGraphData' which better describes the actual behavior. We should only worry about rolling back the interval if 'query.period' has changed. This commit also stops the graph from flickering when it is updated in realtime. * update names of two variables * remove unnecessary negation * make collapsed graph state more explicit * consider stored invalid intervals when graph mounts * fix not showing loading spinner regression * remove interval state from VisitorGraph (#2540) * Realtime prop breakdown (#2535) * disable load more in realtime mode * extract doFetch function * separate fetchPropBreakdown and fetchNextPage functions * subscribe for auto-updates in realtime * improve readability with function name changes
2023-01-02 18:42:57 +03:00
import React, { useState, useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom'
Realtime dashboard improvements (#2445) * add a new realtime-update-timer module * hook to the new 'tick' event in ListReport for auto-updates This commit fixes the bug where all reports using the `ListReport` component did not auto-update in realtime mode. Those reports are: - Pages (Top / Entry / Exit) - Locations (Countries / Regions / Cities) - Devices (Screen Sizes / Browsers + versions / OS-s + versions) * fetch data for ListReports only when scrolled into view * refactor fetching data in ListReport * refer to one source of truth for utm tags * make the 'All' tab in Sources auto-update * make all UTM tabs in Sources auto-update * fetch UTM data only when scrolled into view * auto-update Referrers with the new timer * auto-update google search terms * auto-update Conversions * make countries map auto-update * auto-update visitor-graph and top stats with new timer * use new tick event for current visitors (in Historical) * remove the old timer class * update changelog * Visual improvements to automatic realtime updates (#2532) * minor consistency fix for text color in dark mode * use FlipMove in goal conversions report * use FlipMove in ListReports * set main graph and top stats loading state correctly * refactor isIntervalValid function * enforce intervals are valid when set and stored * remove duplicate data fetching on interval change Fetching new data is handled by the `fetchGraphData` callback in `updateInterval` * refactor updateMetric function * make it clearer why 'metric' can be a faulty value * extract 'query' and 'site' variables from 'this.props' * reset interval state only when period is changed The 'maybeRollbackInterval' function was also used to fetch data. This commit replaces all those function calls with 'fetchGraphData' which better describes the actual behavior. We should only worry about rolling back the interval if 'query.period' has changed. This commit also stops the graph from flickering when it is updated in realtime. * update names of two variables * remove unnecessary negation * make collapsed graph state more explicit * consider stored invalid intervals when graph mounts * fix not showing loading spinner regression * remove interval state from VisitorGraph (#2540) * Realtime prop breakdown (#2535) * disable load more in realtime mode * extract doFetch function * separate fetchPropBreakdown and fetchNextPage functions * subscribe for auto-updates in realtime * improve readability with function name changes
2023-01-02 18:42:57 +03:00
import FlipMove from 'react-flip-move';
import { displayMetricValue, metricLabelFor } from './metrics';
import FadeIn from '../../fade-in'
import MoreLink from '../more-link'
import Bar from '../bar'
import LazyLoader from '../../components/lazy-loader'
import classNames from 'classnames'
import { trimURL } from '../../util/url'
const MAX_ITEMS = 9
const MIN_HEIGHT = 380
const ROW_HEIGHT = 32
const ROW_GAP_HEIGHT = 4
const DATA_CONTAINER_HEIGHT = (ROW_HEIGHT + ROW_GAP_HEIGHT) * (MAX_ITEMS - 1) + ROW_HEIGHT
const COL_MIN_WIDTH = 70
function FilterLink({ filterQuery, onClick, children }) {
const className = classNames('max-w-max w-full flex md:overflow-hidden', {
'hover:underline': !!filterQuery
})
if (filterQuery) {
return (
<Link
to={{ search: filterQuery.toString() }}
onClick={onClick}
className={className}
>
{children}
</Link>
)
} else {
return <span className={className}>{children}</span>
}
}
2021-12-14 12:28:43 +03:00
function ExternalLink({ item, externalLinkDest }) {
const dest = externalLinkDest && externalLinkDest(item)
if (dest) {
2021-12-14 12:28:43 +03:00
return (
<a
target="_blank"
rel="noreferrer"
href={dest}
className="w-4 h-4 hidden group-hover:block"
2021-12-14 12:28:43 +03:00
>
<svg className="inline w-full h-full ml-1 -mt-1 text-gray-600 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"></path><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"></path></svg>
2021-12-14 12:28:43 +03:00
</a>
)
}
2021-12-14 12:28:43 +03:00
return null
}
// The main function component for rendering list reports and making them react to what
// is happening on the dashboard.
// A `fetchData` function must be passed through props. This function defines the format
// of the data, which is expected to be a list of objects. Think of these objects as rows
// with keys being columns. The number of columns is dynamic and should be configured
// via the `metrics` input list. For example:
// | keyLabel | METRIC_1.label | METRIC_2.label | ...
// |--------------------|---------------------------|---------------------------|-----
// | LISTITEM_1.name | LISTITEM_1[METRIC_1.name] | LISTITEM_1[METRIC_2.name] | ...
// | LISTITEM_2.name | LISTITEM_2[METRIC_1.name] | LISTITEM_2[METRIC_2.name] | ...
// Further configuration of the report is possible through optional props.
// REQUIRED PROPS:
// * `keyLabel` - What each entry in the list represents (for UI only).
// * `query` - The query object representing the current state of the dashboard.
// * `fetchData` - a function that returns an `api.get` promise that will resolve to the
// list of data.
// * `metrics` - a list of `metric` objects. Each `metric` object is required to have at
// least the `name` and the `label` keys. If the metric should have a different label
// in realtime or goal-filtered views, we'll use `realtimeLabel` and `GoalFilterLabel`.
// * `getFilterFor` - a function that takes a list item and returns the query link (with
// the filter) to navigate to when this list item is clicked. If a list item is not
// supposed to be clickable, this function should return `null` for that list item.
// OPTIONAL PROPS:
// * `onClick` - function with additional action to be taken when a list entry is clicked.
// * `detailsLink` - the pathname to the detailed view of this report. E.g.:
// `/dummy.site/pages`. If this is given as input to the ListReport, the Details button
// will always be rendered.
// * `maybeHideDetails` - set this to `true` if the details button should be hidden on
// the condition that there are less than MAX_ITEMS entries in the list. (i.e . nothing
// more to show)
// * `externalLinkDest` - a function that takes a list item and returns an external url
// to navigate to. If this prop is given, an additional icon is rendered upon hovering
// the entry.
// * `renderIcon` - a function that takes a list item and returns the
// HTML of an icon (such as a flag, favicon, or a screen size icon) for a listItem.
// * `color` - color of the comparison bars in light-mode
2021-12-14 12:28:43 +03:00
export default function ListReport(props) {
const [state, setState] = useState({ loading: true, list: null })
Realtime dashboard improvements (#2445) * add a new realtime-update-timer module * hook to the new 'tick' event in ListReport for auto-updates This commit fixes the bug where all reports using the `ListReport` component did not auto-update in realtime mode. Those reports are: - Pages (Top / Entry / Exit) - Locations (Countries / Regions / Cities) - Devices (Screen Sizes / Browsers + versions / OS-s + versions) * fetch data for ListReports only when scrolled into view * refactor fetching data in ListReport * refer to one source of truth for utm tags * make the 'All' tab in Sources auto-update * make all UTM tabs in Sources auto-update * fetch UTM data only when scrolled into view * auto-update Referrers with the new timer * auto-update google search terms * auto-update Conversions * make countries map auto-update * auto-update visitor-graph and top stats with new timer * use new tick event for current visitors (in Historical) * remove the old timer class * update changelog * Visual improvements to automatic realtime updates (#2532) * minor consistency fix for text color in dark mode * use FlipMove in goal conversions report * use FlipMove in ListReports * set main graph and top stats loading state correctly * refactor isIntervalValid function * enforce intervals are valid when set and stored * remove duplicate data fetching on interval change Fetching new data is handled by the `fetchGraphData` callback in `updateInterval` * refactor updateMetric function * make it clearer why 'metric' can be a faulty value * extract 'query' and 'site' variables from 'this.props' * reset interval state only when period is changed The 'maybeRollbackInterval' function was also used to fetch data. This commit replaces all those function calls with 'fetchGraphData' which better describes the actual behavior. We should only worry about rolling back the interval if 'query.period' has changed. This commit also stops the graph from flickering when it is updated in realtime. * update names of two variables * remove unnecessary negation * make collapsed graph state more explicit * consider stored invalid intervals when graph mounts * fix not showing loading spinner regression * remove interval state from VisitorGraph (#2540) * Realtime prop breakdown (#2535) * disable load more in realtime mode * extract doFetch function * separate fetchPropBreakdown and fetchNextPage functions * subscribe for auto-updates in realtime * improve readability with function name changes
2023-01-02 18:42:57 +03:00
const [visible, setVisible] = useState(false)
const metrics = props.metrics
const colMinWidth = props.colMinWidth || COL_MIN_WIDTH
const isRealtime = props.query.period === 'realtime'
const goalFilterApplied = !!props.query.filters.goal
2021-12-14 12:28:43 +03:00
Realtime dashboard improvements (#2445) * add a new realtime-update-timer module * hook to the new 'tick' event in ListReport for auto-updates This commit fixes the bug where all reports using the `ListReport` component did not auto-update in realtime mode. Those reports are: - Pages (Top / Entry / Exit) - Locations (Countries / Regions / Cities) - Devices (Screen Sizes / Browsers + versions / OS-s + versions) * fetch data for ListReports only when scrolled into view * refactor fetching data in ListReport * refer to one source of truth for utm tags * make the 'All' tab in Sources auto-update * make all UTM tabs in Sources auto-update * fetch UTM data only when scrolled into view * auto-update Referrers with the new timer * auto-update google search terms * auto-update Conversions * make countries map auto-update * auto-update visitor-graph and top stats with new timer * use new tick event for current visitors (in Historical) * remove the old timer class * update changelog * Visual improvements to automatic realtime updates (#2532) * minor consistency fix for text color in dark mode * use FlipMove in goal conversions report * use FlipMove in ListReports * set main graph and top stats loading state correctly * refactor isIntervalValid function * enforce intervals are valid when set and stored * remove duplicate data fetching on interval change Fetching new data is handled by the `fetchGraphData` callback in `updateInterval` * refactor updateMetric function * make it clearer why 'metric' can be a faulty value * extract 'query' and 'site' variables from 'this.props' * reset interval state only when period is changed The 'maybeRollbackInterval' function was also used to fetch data. This commit replaces all those function calls with 'fetchGraphData' which better describes the actual behavior. We should only worry about rolling back the interval if 'query.period' has changed. This commit also stops the graph from flickering when it is updated in realtime. * update names of two variables * remove unnecessary negation * make collapsed graph state more explicit * consider stored invalid intervals when graph mounts * fix not showing loading spinner regression * remove interval state from VisitorGraph (#2540) * Realtime prop breakdown (#2535) * disable load more in realtime mode * extract doFetch function * separate fetchPropBreakdown and fetchNextPage functions * subscribe for auto-updates in realtime * improve readability with function name changes
2023-01-02 18:42:57 +03:00
const fetchData = useCallback(() => {
if (!isRealtime) {
setState({ loading: true, list: null })
}
props.fetchData()
.then((res) => setState({ loading: false, list: res }))
}, [props.keyLabel, props.query])
const onVisible = () => { setVisible(true) }
Realtime dashboard improvements (#2445) * add a new realtime-update-timer module * hook to the new 'tick' event in ListReport for auto-updates This commit fixes the bug where all reports using the `ListReport` component did not auto-update in realtime mode. Those reports are: - Pages (Top / Entry / Exit) - Locations (Countries / Regions / Cities) - Devices (Screen Sizes / Browsers + versions / OS-s + versions) * fetch data for ListReports only when scrolled into view * refactor fetching data in ListReport * refer to one source of truth for utm tags * make the 'All' tab in Sources auto-update * make all UTM tabs in Sources auto-update * fetch UTM data only when scrolled into view * auto-update Referrers with the new timer * auto-update google search terms * auto-update Conversions * make countries map auto-update * auto-update visitor-graph and top stats with new timer * use new tick event for current visitors (in Historical) * remove the old timer class * update changelog * Visual improvements to automatic realtime updates (#2532) * minor consistency fix for text color in dark mode * use FlipMove in goal conversions report * use FlipMove in ListReports * set main graph and top stats loading state correctly * refactor isIntervalValid function * enforce intervals are valid when set and stored * remove duplicate data fetching on interval change Fetching new data is handled by the `fetchGraphData` callback in `updateInterval` * refactor updateMetric function * make it clearer why 'metric' can be a faulty value * extract 'query' and 'site' variables from 'this.props' * reset interval state only when period is changed The 'maybeRollbackInterval' function was also used to fetch data. This commit replaces all those function calls with 'fetchGraphData' which better describes the actual behavior. We should only worry about rolling back the interval if 'query.period' has changed. This commit also stops the graph from flickering when it is updated in realtime. * update names of two variables * remove unnecessary negation * make collapsed graph state more explicit * consider stored invalid intervals when graph mounts * fix not showing loading spinner regression * remove interval state from VisitorGraph (#2540) * Realtime prop breakdown (#2535) * disable load more in realtime mode * extract doFetch function * separate fetchPropBreakdown and fetchNextPage functions * subscribe for auto-updates in realtime * improve readability with function name changes
2023-01-02 18:42:57 +03:00
useEffect(() => {
if (isRealtime) {
// When a goal filter is applied or removed, we always want the component to go into a
// loading state, even in realtime mode, because the metrics list will change. We can
// only read the new metrics once the new list is loaded.
setState({ loading: true, list: null })
}
}, [goalFilterApplied]);
Realtime dashboard improvements (#2445) * add a new realtime-update-timer module * hook to the new 'tick' event in ListReport for auto-updates This commit fixes the bug where all reports using the `ListReport` component did not auto-update in realtime mode. Those reports are: - Pages (Top / Entry / Exit) - Locations (Countries / Regions / Cities) - Devices (Screen Sizes / Browsers + versions / OS-s + versions) * fetch data for ListReports only when scrolled into view * refactor fetching data in ListReport * refer to one source of truth for utm tags * make the 'All' tab in Sources auto-update * make all UTM tabs in Sources auto-update * fetch UTM data only when scrolled into view * auto-update Referrers with the new timer * auto-update google search terms * auto-update Conversions * make countries map auto-update * auto-update visitor-graph and top stats with new timer * use new tick event for current visitors (in Historical) * remove the old timer class * update changelog * Visual improvements to automatic realtime updates (#2532) * minor consistency fix for text color in dark mode * use FlipMove in goal conversions report * use FlipMove in ListReports * set main graph and top stats loading state correctly * refactor isIntervalValid function * enforce intervals are valid when set and stored * remove duplicate data fetching on interval change Fetching new data is handled by the `fetchGraphData` callback in `updateInterval` * refactor updateMetric function * make it clearer why 'metric' can be a faulty value * extract 'query' and 'site' variables from 'this.props' * reset interval state only when period is changed The 'maybeRollbackInterval' function was also used to fetch data. This commit replaces all those function calls with 'fetchGraphData' which better describes the actual behavior. We should only worry about rolling back the interval if 'query.period' has changed. This commit also stops the graph from flickering when it is updated in realtime. * update names of two variables * remove unnecessary negation * make collapsed graph state more explicit * consider stored invalid intervals when graph mounts * fix not showing loading spinner regression * remove interval state from VisitorGraph (#2540) * Realtime prop breakdown (#2535) * disable load more in realtime mode * extract doFetch function * separate fetchPropBreakdown and fetchNextPage functions * subscribe for auto-updates in realtime * improve readability with function name changes
2023-01-02 18:42:57 +03:00
useEffect(() => {
if (visible) {
if (isRealtime) { document.addEventListener('tick', fetchData) }
fetchData()
}
Realtime dashboard improvements (#2445) * add a new realtime-update-timer module * hook to the new 'tick' event in ListReport for auto-updates This commit fixes the bug where all reports using the `ListReport` component did not auto-update in realtime mode. Those reports are: - Pages (Top / Entry / Exit) - Locations (Countries / Regions / Cities) - Devices (Screen Sizes / Browsers + versions / OS-s + versions) * fetch data for ListReports only when scrolled into view * refactor fetching data in ListReport * refer to one source of truth for utm tags * make the 'All' tab in Sources auto-update * make all UTM tabs in Sources auto-update * fetch UTM data only when scrolled into view * auto-update Referrers with the new timer * auto-update google search terms * auto-update Conversions * make countries map auto-update * auto-update visitor-graph and top stats with new timer * use new tick event for current visitors (in Historical) * remove the old timer class * update changelog * Visual improvements to automatic realtime updates (#2532) * minor consistency fix for text color in dark mode * use FlipMove in goal conversions report * use FlipMove in ListReports * set main graph and top stats loading state correctly * refactor isIntervalValid function * enforce intervals are valid when set and stored * remove duplicate data fetching on interval change Fetching new data is handled by the `fetchGraphData` callback in `updateInterval` * refactor updateMetric function * make it clearer why 'metric' can be a faulty value * extract 'query' and 'site' variables from 'this.props' * reset interval state only when period is changed The 'maybeRollbackInterval' function was also used to fetch data. This commit replaces all those function calls with 'fetchGraphData' which better describes the actual behavior. We should only worry about rolling back the interval if 'query.period' has changed. This commit also stops the graph from flickering when it is updated in realtime. * update names of two variables * remove unnecessary negation * make collapsed graph state more explicit * consider stored invalid intervals when graph mounts * fix not showing loading spinner regression * remove interval state from VisitorGraph (#2540) * Realtime prop breakdown (#2535) * disable load more in realtime mode * extract doFetch function * separate fetchPropBreakdown and fetchNextPage functions * subscribe for auto-updates in realtime * improve readability with function name changes
2023-01-02 18:42:57 +03:00
return () => { document.removeEventListener('tick', fetchData) }
}, [props.keyLabel, props.query, visible]);
2021-12-14 12:28:43 +03:00
// returns a filtered `metrics` list. Since currently, the backend can return different
// metrics based on filters and existing data, this function validates that the metrics
// we want to display are actually there in the API response.
function getAvailableMetrics() {
return metrics.filter((metric) => {
return state.list.some((listItem) => listItem[metric.name] != null)
})
}
function hiddenOnMobileClass(metric) {
if (metric.hiddenOnMobile) {
return 'hidden md:block'
} else {
return ''
}
}
function renderReport() {
if (state.list && state.list.length > 0) {
return (
<div className="h-full flex flex-col">
<div style={{ height: ROW_HEIGHT }}>
{renderReportHeader()}
</div>
<div style={{ minHeight: DATA_CONTAINER_HEIGHT }}>
{renderReportBody()}
</div>
{maybeRenderDetailsLink()}
</div>
)
}
return renderNoDataYet()
}
function renderReportHeader() {
const metricLabels = getAvailableMetrics().map((metric) => {
return (
<div
key={metric.name}
className={`text-right ${hiddenOnMobileClass(metric)}`}
style={{ minWidth: colMinWidth }}
>
{metricLabelFor(metric, props.query)}
</div>
)
})
return (
<div className="pt-3 w-full text-xs font-bold tracking-wide text-gray-500 flex items-center dark:text-gray-400">
<span className="flex-grow truncate">{props.keyLabel}</span>
{metricLabels}
</div>
)
}
function renderReportBody() {
return (
<FlipMove className="flex-grow">
{state.list.slice(0, MAX_ITEMS).map(renderRow)}
</FlipMove>
)
}
function renderRow(listItem) {
return (
<div key={listItem.name} style={{ minHeight: ROW_HEIGHT }}>
<div className="flex w-full" style={{ marginTop: ROW_GAP_HEIGHT }}>
{renderBarFor(listItem)}
{renderMetricValuesFor(listItem)}
</div>
</div>
)
}
function getFilterQuery(listItem) {
const filter = props.getFilterFor(listItem)
if (!filter) { return null }
const query = new URLSearchParams(window.location.search)
Object.entries(filter).forEach((([key, value]) => {
query.set(key, value)
}))
return query
}
function renderBarFor(listItem) {
2021-12-14 12:28:43 +03:00
const lightBackground = props.color || 'bg-green-50'
const noop = () => { }
const metricToPlot = metrics.find(m => m.plot).name
return (
<div className="flex-grow w-full overflow-hidden">
<Bar
count={listItem[metricToPlot]}
2021-12-14 12:28:43 +03:00
all={state.list}
bg={`${lightBackground} dark:bg-gray-500 dark:bg-opacity-15`}
plot={metricToPlot}
>
<div className="flex justify-start px-2 py-1.5 group text-sm dark:text-gray-300 relative z-9 break-all w-full">
<FilterLink filterQuery={getFilterQuery(listItem)} onClick={props.onClick || noop}>
{maybeRenderIconFor(listItem)}
<span className="w-full md:truncate">
{trimURL(listItem.name, colMinWidth)}
</span>
</FilterLink>
<ExternalLink item={listItem} externalLinkDest={props.externalLinkDest} />
</div>
</Bar>
</div>
)
}
function maybeRenderIconFor(listItem) {
if (props.renderIcon) {
return (
<span className="pr-1">
{props.renderIcon(listItem)}
</span>
)
}
}
function renderMetricValuesFor(listItem) {
return getAvailableMetrics().map((metric) => {
return (
<div
key={`${listItem.name}__${metric.name}`}
className={`text-right ${hiddenOnMobileClass(metric)}`}
style={{ width: colMinWidth, minWidth: colMinWidth }}
>
<span className="font-medium text-sm dark:text-gray-200 text-right">
{displayMetricValue(listItem[metric.name], metric)}
</span>
</div>
)
})
}
function renderLoading() {
return (
<div className="w-full flex flex-col justify-center" style={{ minHeight: `${MIN_HEIGHT}px` }}>
<div className="mx-auto loading"><div></div></div>
</div>
)
}
function renderNoDataYet() {
return (
<div className="w-full h-full flex flex-col justify-center" style={{ minHeight: `${MIN_HEIGHT}px` }}>
<div className="mx-auto font-medium text-gray-500 dark:text-gray-400">No data yet</div>
</div>
)
}
function maybeRenderDetailsLink() {
const moreResultsAvailable = state.list.length >= MAX_ITEMS
const hideDetails = props.maybeHideDetails && !moreResultsAvailable
const showDetails = props.detailsLink && !state.loading && !hideDetails
return showDetails && <MoreLink className={'mt-2'} url={props.detailsLink} list={state.list} />
}
2021-12-14 12:28:43 +03:00
return (
<LazyLoader onVisible={onVisible} >
<div className="w-full" style={{ minHeight: `${MIN_HEIGHT}px` }}>
{state.loading && renderLoading()}
{!state.loading && <FadeIn show={!state.loading} className="h-full">
{renderReport()}
</FadeIn>}
</div>
2021-12-14 12:28:43 +03:00
</LazyLoader>
)
}