This is an automated email from the ASF dual-hosted git repository.

vogievetsky pushed a commit to branch segment_timeline2
in repository https://gitbox.apache.org/repos/asf/druid.git

commit fb7371370a3987695141608167b35266946aa777
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Tue Oct 22 14:34:57 2024 -0700

    init refactor of segment timeline
---
 web-console/lib/keywords.ts                        |   8 ++
 .../src/components/segment-timeline/bar-group.tsx  |  19 ++--
 .../components/segment-timeline/bar-unit.spec.tsx  |  33 -------
 .../src/components/segment-timeline/chart-axis.tsx |   7 +-
 .../segment-timeline/{bar-unit.tsx => common.ts}   |  45 ++++-----
 .../segment-timeline/segment-timeline.scss         |   8 +-
 .../segment-timeline/segment-timeline.tsx          |  83 ++++++----------
 .../segment-timeline/stacked-bar-chart.tsx         | 104 ++++++++-------------
 .../views/datasources-view/datasources-view.tsx    |   2 +-
 9 files changed, 119 insertions(+), 190 deletions(-)

diff --git a/web-console/lib/keywords.ts b/web-console/lib/keywords.ts
index 06d7ccdcc94..5985ae0a6aa 100644
--- a/web-console/lib/keywords.ts
+++ b/web-console/lib/keywords.ts
@@ -100,15 +100,23 @@ export const SQL_EXPRESSION_PARTS = [
   'TRAILING',
   'EPOCH',
   'SECOND',
+  'SECONDS',
   'MINUTE',
+  'MINUTES',
   'HOUR',
+  'HOURS',
   'DAY',
+  'DAYS',
   'DOW',
   'DOY',
   'WEEK',
+  'WEEKS',
   'MONTH',
+  'MONTHS',
   'QUARTER',
+  'QUARTERS',
   'YEAR',
+  'YEARS',
   'TIMESTAMP',
   'INTERVAL',
   'CSV',
diff --git a/web-console/src/components/segment-timeline/bar-group.tsx 
b/web-console/src/components/segment-timeline/bar-group.tsx
index d0cf867e2b2..3c61859115a 100644
--- a/web-console/src/components/segment-timeline/bar-group.tsx
+++ b/web-console/src/components/segment-timeline/bar-group.tsx
@@ -19,26 +19,18 @@
 import type { AxisScale } from 'd3-axis';
 import React from 'react';
 
-import { BarUnit } from './bar-unit';
-import type { BarUnitData, HoveredBarInfo } from './stacked-bar-chart';
+import type { BarUnitData, HoveredBarInfo } from './common';
 
 interface BarGroupProps {
   dataToRender: BarUnitData[];
   changeActiveDatasource: (dataSource: string) => void;
-  formatTick: (e: number) => string;
   xScale: AxisScale<Date>;
   yScale: AxisScale<number>;
   barWidth: number;
-  onHoverBar?: (e: any) => void;
-  offHoverBar?: () => void;
-  hoverOn?: HoveredBarInfo | null;
+  onHoverBar: (e: HoveredBarInfo) => void;
 }
 
 export class BarGroup extends React.Component<BarGroupProps> {
-  shouldComponentUpdate(nextProps: BarGroupProps): boolean {
-    return nextProps.hoverOn === this.props.hoverOn;
-  }
-
   render() {
     const { dataToRender, changeActiveDatasource, xScale, yScale, onHoverBar, 
barWidth } =
       this.props;
@@ -47,6 +39,8 @@ export class BarGroup extends React.Component<BarGroupProps> {
     return dataToRender.map((entry: BarUnitData, i: number) => {
       const y0 = yScale(entry.y0 || 0) || 0;
       const x = xScale(new Date(entry.x + 'T00:00:00Z'));
+      if (typeof x === 'undefined') return;
+
       const y = yScale((entry.y0 || 0) + entry.y) || 0;
       const height = Math.max(y0 - y, 0);
       const barInfo: HoveredBarInfo = {
@@ -59,15 +53,16 @@ export class BarGroup extends 
React.Component<BarGroupProps> {
         dailySize: entry.dailySize,
       };
       return (
-        <BarUnit
+        <rect
           key={i}
+          className="bar-unit"
           x={x}
           y={y}
           width={barWidth}
           height={height}
           style={{ fill: entry.color }}
           onClick={() => changeActiveDatasource(entry.datasource)}
-          onHover={() => onHoverBar && onHoverBar(barInfo)}
+          onMouseOver={() => onHoverBar(barInfo)}
         />
       );
     });
diff --git a/web-console/src/components/segment-timeline/bar-unit.spec.tsx 
b/web-console/src/components/segment-timeline/bar-unit.spec.tsx
deleted file mode 100644
index d5926dcf69f..00000000000
--- a/web-console/src/components/segment-timeline/bar-unit.spec.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { render } from '@testing-library/react';
-
-import { BarUnit } from './bar-unit';
-
-describe('BarUnit', () => {
-  it('matches snapshot', () => {
-    const barGroup = (
-      <svg>
-        <BarUnit x={10} y={10} width={10} height={10} />
-      </svg>
-    );
-    const { container } = render(barGroup);
-    expect(container.firstChild).toMatchSnapshot();
-  });
-});
diff --git a/web-console/src/components/segment-timeline/chart-axis.tsx 
b/web-console/src/components/segment-timeline/chart-axis.tsx
index bc333d33b77..18dc7d3e076 100644
--- a/web-console/src/components/segment-timeline/chart-axis.tsx
+++ b/web-console/src/components/segment-timeline/chart-axis.tsx
@@ -16,22 +16,23 @@
  * limitations under the License.
  */
 
+import type { Axis } from 'd3-axis';
 import { select } from 'd3-selection';
 import React from 'react';
 
 interface ChartAxisProps {
   transform?: string;
-  scale: any;
+  axis: Axis<any>;
   className?: string;
 }
 
 export const ChartAxis = React.memo(function ChartAxis(props: ChartAxisProps) {
-  const { transform, scale, className } = props;
+  const { transform, axis, className } = props;
   return (
     <g
       className={`chart-axis ${className}`}
       transform={transform}
-      ref={node => select(node).call(scale)}
+      ref={node => select(node).call(axis as any)}
     />
   );
 });
diff --git a/web-console/src/components/segment-timeline/bar-unit.tsx 
b/web-console/src/components/segment-timeline/common.ts
similarity index 63%
rename from web-console/src/components/segment-timeline/bar-unit.tsx
rename to web-console/src/components/segment-timeline/common.ts
index 8591f68cc2e..9add957e2c1 100644
--- a/web-console/src/components/segment-timeline/bar-unit.tsx
+++ b/web-console/src/components/segment-timeline/common.ts
@@ -16,30 +16,31 @@
  * limitations under the License.
  */
 
-interface BarChartUnitProps {
-  x: number | undefined;
+export interface BarUnitData {
+  x: number;
   y: number;
+  y0?: number;
   width: number;
-  height: number;
-  style?: any;
-  onClick?: () => void;
-  onHover?: () => void;
-  offHover?: () => void;
+  datasource: string;
+  color: string;
+  dailySize: number;
 }
 
-export function BarUnit(props: BarChartUnitProps) {
-  const { x, y, width, height, style, onClick, onHover, offHover } = props;
-  return (
-    <rect
-      className="bar-unit"
-      x={x}
-      y={y}
-      width={width}
-      height={height}
-      style={style}
-      onClick={onClick}
-      onMouseOver={onHover}
-      onMouseLeave={offHover}
-    />
-  );
+export interface Margin {
+  top: number;
+  right: number;
+  bottom: number;
+  left: number;
 }
+
+export interface HoveredBarInfo {
+  xCoordinate: number;
+  yCoordinate: number;
+  height: number;
+  datasource: string;
+  xValue: number;
+  yValue: number;
+  dailySize: number;
+}
+
+export type SegmentStat = 'sizeData' | 'countData';
diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss 
b/web-console/src/components/segment-timeline/segment-timeline.scss
index aa437052d61..24ee569ad3a 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.scss
+++ b/web-console/src/components/segment-timeline/segment-timeline.scss
@@ -16,9 +16,12 @@
  * limitations under the License.
  */
 
+@import '../../variables';
+
 .segment-timeline {
   display: grid;
-  grid-template-columns: 1fr 220px;
+  grid-template-columns: 1fr 240px;
+  gap: 8px;
 
   .loader {
     width: 85%;
@@ -39,6 +42,7 @@
   }
 
   .side-control {
-    padding-top: 20px;
+    @include card-like;
+    padding: 10px;
   }
 }
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx 
b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 8aee0c66d47..f59a59e2399 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -31,8 +31,6 @@ import type { Capabilities } from '../../helpers';
 import { Api } from '../../singletons';
 import {
   ceilToUtcDay,
-  formatBytes,
-  formatInteger,
   isNonNullRange,
   localToUtcDateRange,
   queryDruidSql,
@@ -42,7 +40,7 @@ import {
 } from '../../utils';
 import { Loader } from '../loader/loader';
 
-import type { BarUnitData } from './stacked-bar-chart';
+import type { BarUnitData, SegmentStat } from './common';
 import { StackedBarChart } from './stacked-bar-chart';
 
 import './segment-timeline.scss';
@@ -51,8 +49,6 @@ interface SegmentTimelineProps {
   capabilities: Capabilities;
 }
 
-type ActiveDataType = 'sizeData' | 'countData';
-
 interface SegmentTimelineState {
   chartHeight: number;
   chartWidth: number;
@@ -60,8 +56,8 @@ interface SegmentTimelineState {
   datasources: string[];
   stackedData?: Record<string, BarUnitData[]>;
   singleDatasourceData?: Record<string, Record<string, BarUnitData[]>>;
-  activeDatasource: string | null;
-  activeDataType: ActiveDataType;
+  activeDatasource?: string;
+  activeSegmentStat: SegmentStat;
   dataToRender: BarUnitData[];
   loading: boolean;
   error?: Error;
@@ -246,7 +242,7 @@ ORDER BY "start" DESC`;
     any
   >;
 
-  private readonly chartMargin = { top: 40, right: 15, bottom: 20, left: 60 };
+  private readonly chartMargin = { top: 40, right: 0, bottom: 20, left: 60 };
 
   constructor(props: SegmentTimelineProps) {
     super(props);
@@ -260,8 +256,7 @@ ORDER BY "start" DESC`;
       stackedData: {},
       singleDatasourceData: {},
       dataToRender: [],
-      activeDatasource: null,
-      activeDataType: 'sizeData',
+      activeSegmentStat: 'sizeData',
       loading: true,
       xScale: null,
       yScale: null,
@@ -355,10 +350,10 @@ ORDER BY "start" DESC`;
   }
 
   componentDidUpdate(_prevProps: SegmentTimelineProps, prevState: 
SegmentTimelineState): void {
-    const { activeDatasource, activeDataType, singleDatasourceData, 
stackedData } = this.state;
+    const { activeDatasource, activeSegmentStat, singleDatasourceData, 
stackedData } = this.state;
     if (
       prevState.data !== this.state.data ||
-      prevState.activeDataType !== this.state.activeDataType ||
+      prevState.activeSegmentStat !== this.state.activeSegmentStat ||
       prevState.activeDatasource !== this.state.activeDatasource ||
       prevState.chartWidth !== this.state.chartWidth ||
       prevState.chartHeight !== this.state.chartHeight
@@ -366,10 +361,10 @@ ORDER BY "start" DESC`;
       const scales: BarChartScales | undefined = this.calculateScales();
       const dataToRender: BarUnitData[] | undefined = activeDatasource
         ? singleDatasourceData
-          ? singleDatasourceData[activeDataType][activeDatasource]
+          ? singleDatasourceData[activeSegmentStat][activeDatasource]
           : undefined
         : stackedData
-        ? stackedData[activeDataType]
+        ? stackedData[activeSegmentStat]
         : undefined;
 
       if (scales && dataToRender) {
@@ -387,31 +382,26 @@ ORDER BY "start" DESC`;
       chartWidth,
       chartHeight,
       data,
-      activeDataType,
+      activeSegmentStat,
       activeDatasource,
       singleDatasourceData,
       dateRange,
     } = this.state;
     if (!data || !Object.keys(data).length || !isNonNullRange(dateRange)) 
return;
-    const activeData = data[activeDataType];
+    const activeData = data[activeSegmentStat];
 
-    let yDomain: number[] = [
-      0,
+    let yMax =
       activeData.length === 0
-        ? 0
-        : activeData.reduce((max: any, d: any) => (max.total > d.total ? max : 
d)).total,
-    ];
+        ? 100
+        : activeData.reduce((max: any, d: any) => (max.total > d.total ? max : 
d)).total;
 
     if (
-      activeDatasource !== null &&
-      singleDatasourceData![activeDataType][activeDatasource] !== undefined
+      activeDatasource &&
+      singleDatasourceData![activeSegmentStat][activeDatasource] !== undefined
     ) {
-      yDomain = [
-        0,
-        singleDatasourceData![activeDataType][activeDatasource].reduce((max: 
any, d: any) =>
-          max.y > d.y ? max : d,
-        ).y,
-      ];
+      yMax = 
singleDatasourceData![activeSegmentStat][activeDatasource].reduce((max: any, d: 
any) =>
+        max.y > d.y ? max : d,
+      ).y;
     }
 
     const xScale: AxisScale<Date> = scaleUtc()
@@ -420,7 +410,7 @@ ORDER BY "start" DESC`;
 
     const yScale: AxisScale<number> = scaleLinear()
       .rangeRound([chartHeight - this.chartMargin.top - 
this.chartMargin.bottom, 0])
-      .domain(yDomain);
+      .domain([0, yMax]);
 
     return {
       xScale,
@@ -428,16 +418,6 @@ ORDER BY "start" DESC`;
     };
   }
 
-  private readonly formatTick = (n: number) => {
-    if (isNaN(n)) return '';
-    const { activeDataType } = this.state;
-    if (activeDataType === 'countData') {
-      return formatInteger(n);
-    } else {
-      return formatBytes(n);
-    }
-  };
-
   private readonly handleResize = (entries: ResizeObserverEntry[]) => {
     const chartRect = entries[0].contentRect;
     this.setState({
@@ -452,7 +432,7 @@ ORDER BY "start" DESC`;
       chartHeight,
       loading,
       dataToRender,
-      activeDataType,
+      activeSegmentStat,
       error,
       xScale,
       yScale,
@@ -485,7 +465,7 @@ ORDER BY "start" DESC`;
       );
     }
 
-    if (data![activeDataType].length === 0) {
+    if (data![activeSegmentStat].length === 0) {
       return (
         <div>
           <span className="no-data-text">There are no segments for the 
selected interval</span>
@@ -494,8 +474,8 @@ ORDER BY "start" DESC`;
     }
 
     if (
-      activeDatasource !== null &&
-      data![activeDataType].every((d: any) => d[activeDatasource] === 
undefined)
+      activeDatasource &&
+      data![activeSegmentStat].every((d: any) => d[activeDatasource] === 
undefined)
     ) {
       return (
         <div>
@@ -519,13 +499,12 @@ ORDER BY "start" DESC`;
           svgHeight={chartHeight}
           svgWidth={chartWidth}
           margin={this.chartMargin}
-          changeActiveDatasource={(datasource: string | null) =>
+          changeActiveDatasource={(datasource: string | undefined) =>
             this.setState(prevState => ({
-              activeDatasource: prevState.activeDatasource ? null : datasource,
+              activeDatasource: prevState.activeDatasource ? undefined : 
datasource,
             }))
           }
-          activeDataType={activeDataType}
-          formatTick={(n: number) => this.formatTick(n)}
+          shownSegmentStat={activeSegmentStat}
           xScale={xScale}
           yScale={yScale}
           barWidth={barWidth}
@@ -536,7 +515,7 @@ ORDER BY "start" DESC`;
 
   render() {
     const { capabilities } = this.props;
-    const { datasources, activeDataType, activeDatasource, dateRange, 
selectedDateRange } =
+    const { datasources, activeSegmentStat, activeDatasource, dateRange, 
selectedDateRange } =
       this.state;
 
     const filterDatasource: ItemPredicate<string> = (query, val, _index, 
exactMatch) => {
@@ -574,7 +553,7 @@ ORDER BY "start" DESC`;
       const showAll = 'Show all';
       const handleItemSelected = (selectedItem: string) => {
         this.setState({
-          activeDatasource: selectedItem === showAll ? null : selectedItem,
+          activeDatasource: selectedItem === showAll ? undefined : 
selectedItem,
         });
       };
       const datasourcesWzAll = [showAll].concat(datasources);
@@ -602,9 +581,9 @@ ORDER BY "start" DESC`;
         <div className="side-control">
           <FormGroup label="Show">
             <SegmentedControl
-              value={activeDataType}
+              value={activeSegmentStat}
               onValueChange={activeDataType =>
-                this.setState({ activeDataType: activeDataType as 
ActiveDataType })
+                this.setState({ activeSegmentStat: activeDataType as 
SegmentStat })
               }
               options={[
                 {
diff --git a/web-console/src/components/segment-timeline/stacked-bar-chart.tsx 
b/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
index 8018aaee5f6..571c50f65a5 100644
--- a/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
@@ -20,47 +20,21 @@ import type { AxisScale } from 'd3-axis';
 import { axisBottom, axisLeft } from 'd3-axis';
 import React, { useState } from 'react';
 
+import { formatBytes, formatInteger } from '../../utils';
+
 import { BarGroup } from './bar-group';
 import { ChartAxis } from './chart-axis';
+import type { BarUnitData, HoveredBarInfo, Margin, SegmentStat } from 
'./common';
 
 import './stacked-bar-chart.scss';
 
-export interface BarUnitData {
-  x: number;
-  y: number;
-  y0?: number;
-  width: number;
-  datasource: string;
-  color: string;
-  dailySize?: number;
-}
-
-export interface BarChartMargin {
-  top: number;
-  right: number;
-  bottom: number;
-  left: number;
-}
-
-export interface HoveredBarInfo {
-  xCoordinate?: number;
-  yCoordinate?: number;
-  height?: number;
-  width?: number;
-  datasource?: string;
-  xValue?: number;
-  yValue?: number;
-  dailySize?: number;
-}
-
 interface StackedBarChartProps {
   svgWidth: number;
   svgHeight: number;
-  margin: BarChartMargin;
-  activeDataType?: string;
+  margin: Margin;
+  shownSegmentStat: SegmentStat;
   dataToRender: BarUnitData[];
-  changeActiveDatasource: (e: string | null) => void;
-  formatTick: (e: number) => string;
+  changeActiveDatasource: (e: string | undefined) => void;
   xScale: AxisScale<Date>;
   yScale: AxisScale<number>;
   barWidth: number;
@@ -71,11 +45,10 @@ export const StackedBarChart = React.forwardRef(function 
StackedBarChart(
   ref,
 ) {
   const {
-    activeDataType,
+    shownSegmentStat,
     svgWidth,
     svgHeight,
     margin,
-    formatTick,
     xScale,
     yScale,
     dataToRender,
@@ -84,11 +57,36 @@ export const StackedBarChart = React.forwardRef(function 
StackedBarChart(
   } = props;
   const [hoverOn, setHoverOn] = useState<HoveredBarInfo>();
 
+  const formatTick = (n: number) => {
+    if (isNaN(n)) return '';
+    if (shownSegmentStat === 'countData') {
+      return formatInteger(n);
+    } else {
+      return formatBytes(n);
+    }
+  };
+
   const width = svgWidth - margin.left - margin.right;
   const height = svgHeight - margin.top - margin.bottom;
 
-  function renderBarChart() {
-    return (
+  return (
+    <div className="stacked-bar-chart" ref={ref as any}>
+      {hoverOn && (
+        <div className="bar-chart-tooltip">
+          <div>Datasource: {hoverOn.datasource}</div>
+          <div>Time: {hoverOn.xValue}</div>
+          <div>
+            {`${
+              shownSegmentStat === 'countData' ? 'Daily total count:' : 'Daily 
total size:'
+            } ${formatTick(hoverOn.dailySize)}`}
+          </div>
+          <div>
+            {`${shownSegmentStat === 'countData' ? 'Count:' : 'Size:'} 
${formatTick(
+              hoverOn.yValue,
+            )}`}
+          </div>
+        </div>
+      )}
       <svg
         width={svgWidth}
         height={svgHeight}
@@ -102,7 +100,7 @@ export const StackedBarChart = React.forwardRef(function 
StackedBarChart(
           <ChartAxis
             className="gridline-x"
             transform="translate(0, 0)"
-            scale={axisLeft(yScale)
+            axis={axisLeft(yScale)
               .ticks(5)
               .tickSize(-width)
               .tickFormat(() => '')
@@ -111,30 +109,28 @@ export const StackedBarChart = React.forwardRef(function 
StackedBarChart(
           <BarGroup
             dataToRender={dataToRender}
             changeActiveDatasource={changeActiveDatasource}
-            formatTick={formatTick}
             xScale={xScale}
             yScale={yScale}
             onHoverBar={(e: HoveredBarInfo) => setHoverOn(e)}
-            hoverOn={hoverOn}
             barWidth={barWidth}
           />
           <ChartAxis
             className="axis-x"
             transform={`translate(0, ${height})`}
-            scale={axisBottom(xScale)}
+            axis={axisBottom(xScale)}
           />
           <ChartAxis
             className="axis-y"
-            scale={axisLeft(yScale)
+            axis={axisLeft(yScale)
               .ticks(5)
-              .tickFormat((e: number) => formatTick(e))}
+              .tickFormat(e => formatTick(e))}
           />
           {hoverOn && (
             <g
               className="hovered-bar"
               onClick={() => {
                 setHoverOn(undefined);
-                changeActiveDatasource(hoverOn.datasource ?? null);
+                changeActiveDatasource(hoverOn.datasource);
               }}
             >
               <rect
@@ -147,28 +143,6 @@ export const StackedBarChart = React.forwardRef(function 
StackedBarChart(
           )}
         </g>
       </svg>
-    );
-  }
-
-  return (
-    <div className="stacked-bar-chart" ref={ref as any}>
-      {hoverOn && (
-        <div className="bar-chart-tooltip">
-          <div>Datasource: {hoverOn.datasource}</div>
-          <div>Time: {hoverOn.xValue}</div>
-          <div>
-            {`${
-              activeDataType === 'countData' ? 'Daily total count:' : 'Daily 
total size:'
-            } ${formatTick(hoverOn.dailySize!)}`}
-          </div>
-          <div>
-            {`${activeDataType === 'countData' ? 'Count:' : 'Size:'} 
${formatTick(
-              hoverOn.yValue!,
-            )}`}
-          </div>
-        </div>
-      )}
-      {renderBarChart()}
     </div>
   );
 });
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx 
b/web-console/src/views/datasources-view/datasources-view.tsx
index 3fb6a5be40d..cafc69cade5 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -418,7 +418,7 @@ GROUP BY 1, 2`;
         LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION,
         ['Segment size', 'Segment granularity'],
       ),
-      showSegmentTimeline: false,
+      showSegmentTimeline: true,
 
       actions: [],
     };


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to